diff --git a/build/win32/code.iss b/build/win32/code.iss index d64516e6a87..5f53bc3375d 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -1528,11 +1528,12 @@ begin begin CreateMutex('{#AppMutex}-ready'); + Log('Checking whether application is still running...'); while (CheckForMutexes('{#AppMutex}')) do begin - Log('Application is still running, waiting'); Sleep(1000) end; + Log('Application appears not to be running.'); StopTunnelServiceIfNeeded(); diff --git a/extensions/git/package.json b/extensions/git/package.json index e42d7b9c393..797dc472f1f 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3006,7 +3006,7 @@ }, "dependencies": { "@joaomoreno/unique-names-generator": "^5.1.0", - "@vscode/extension-telemetry": "^0.8.4", + "@vscode/extension-telemetry": "^0.8.5", "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 0b62d7472be..8c97077c60c 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -306,10 +306,10 @@ resolved "https://registry.yarnpkg.com/@types/which/-/which-3.0.0.tgz#849afdd9fdcb0b67339b9cfc80fa6ea4e0253fc5" integrity sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 4855716e08e..c1e13b86e2a 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -60,7 +60,7 @@ }, "dependencies": { "node-fetch": "2.6.7", - "@vscode/extension-telemetry": "^0.8.4", + "@vscode/extension-telemetry": "^0.8.5", "vscode-tas-client": "^0.1.47" }, "devDependencies": { diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index da5f5631576..e8c7997aa38 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -282,10 +282,10 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.2.tgz#93eb2c243c351f3f17d5c580c7467ae5d686b65f" integrity sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/github/package.json b/extensions/github/package.json index afbb7253f7f..e54ba0dc972 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -183,7 +183,7 @@ "@octokit/graphql-schema": "14.4.0", "@octokit/rest": "19.0.4", "tunnel": "^0.0.6", - "@vscode/extension-telemetry": "^0.8.4" + "@vscode/extension-telemetry": "^0.8.5" }, "devDependencies": { "@types/node": "18.x" diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index ab4275a38b9..dcf0f5c776b 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -386,10 +386,10 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.2.tgz#93eb2c243c351f3f17d5c580c7467ae5d686b65f" integrity sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index a18aed6e955..26f60e495f6 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -258,7 +258,7 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "^0.8.4", + "@vscode/extension-telemetry": "^0.8.5", "vscode-languageclient": "^8.2.0-next.3", "vscode-uri": "^3.0.7" }, diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index 57d4562ac85..944f5432540 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -269,10 +269,10 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.2.tgz#93eb2c243c351f3f17d5c580c7467ae5d686b65f" integrity sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index f804c30da79..a701afdd503 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -158,7 +158,7 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "^0.8.4", + "@vscode/extension-telemetry": "^0.8.5", "request-light": "^0.7.0", "vscode-languageclient": "^8.2.0-next.3" }, diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 1ea64055bdd..79674bc920d 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -269,10 +269,10 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.2.tgz#93eb2c243c351f3f17d5c580c7467ae5d686b65f" integrity sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 9743845b8d9..c4507c0b30c 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -726,7 +726,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "^0.8.4", + "@vscode/extension-telemetry": "^0.8.5", "dompurify": "^3.0.5", "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 36b5aebfed8..24b0cbacda6 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -321,10 +321,10 @@ resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf" integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/media-preview/package.json b/extensions/media-preview/package.json index 6c1b46220b9..7eefc696310 100644 --- a/extensions/media-preview/package.json +++ b/extensions/media-preview/package.json @@ -126,7 +126,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "^0.8.4", + "@vscode/extension-telemetry": "^0.8.5", "vscode-uri": "^3.0.6" }, "repository": { diff --git a/extensions/media-preview/yarn.lock b/extensions/media-preview/yarn.lock index 36b652d7d7b..543b4af6f5d 100644 --- a/extensions/media-preview/yarn.lock +++ b/extensions/media-preview/yarn.lock @@ -264,10 +264,10 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.2.tgz#93eb2c243c351f3f17d5c580c7467ae5d686b65f" integrity sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index 751e85c3da2..41c6c8a5aa6 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -166,7 +166,7 @@ } }, "dependencies": { - "@vscode/extension-telemetry": "^0.8.4" + "@vscode/extension-telemetry": "^0.8.5" }, "devDependencies": { "@types/node": "18.x" diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index d214c5e4447..788a65630a4 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -269,10 +269,10 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.2.tgz#93eb2c243c351f3f17d5c580c7467ae5d686b65f" integrity sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 8747c4a1df8..bb516abea4c 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -118,7 +118,7 @@ "dependencies": { "node-fetch": "2.6.7", "@azure/ms-rest-azure-env": "^2.0.0", - "@vscode/extension-telemetry": "^0.8.4" + "@vscode/extension-telemetry": "^0.8.5" }, "repository": { "type": "git", diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index 68b4aa00b84..50b1ba97f3d 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -306,10 +306,10 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index e86b8de070b..137147ff5a4 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -66,7 +66,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "^0.8.4" + "@vscode/extension-telemetry": "^0.8.5" }, "devDependencies": { "@types/vscode-webview": "^1.57.0", diff --git a/extensions/simple-browser/yarn.lock b/extensions/simple-browser/yarn.lock index d6ce34c9e21..18cb555aee2 100644 --- a/extensions/simple-browser/yarn.lock +++ b/extensions/simple-browser/yarn.lock @@ -269,10 +269,10 @@ resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf" integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 7f866fa961d..607bb3aabe0 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -34,7 +34,7 @@ "Programming Languages" ], "dependencies": { - "@vscode/extension-telemetry": "^0.8.4", + "@vscode/extension-telemetry": "^0.8.5", "@vscode/sync-api-client": "^0.7.2", "@vscode/sync-api-common": "^0.7.2", "@vscode/sync-api-service": "^0.7.3", diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 710e6c54132..df435137900 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -253,10 +253,10 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.2.tgz#93eb2c243c351f3f17d5c580c7467ae5d686b65f" integrity sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg== -"@vscode/extension-telemetry@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz#c078c6f55df1c9e0592de3b4ce0f685dd345bfe7" - integrity sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA== +"@vscode/extension-telemetry@^0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.8.5.tgz#3db305be907c01656160e25d91f5d2840175d199" + integrity sha512-YFKANBT2F3qdWQstjcr40XX8BLsdKlKM7a7YPi/jNuMjuiPhb1Jn7YsDR3WZaVEzAqeqGy4gzXsFCBbuZ+L1Tg== dependencies: "@microsoft/1ds-core-js" "^3.2.13" "@microsoft/1ds-post-js" "^3.2.13" diff --git a/package.json b/package.json index 1940684eb1b..e4e125925a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.83.0", - "distro": "5a794f00ac87e37b6abea46ccf030aeb4b323aeb", + "distro": "f4e12eeb4a251bbec78e035eaeb7f7c5dd372a40", "author": { "name": "Microsoft Corporation" }, @@ -94,14 +94,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.4.0-beta.19", - "xterm-addon-canvas": "0.6.0-beta.19", + "xterm": "5.4.0-beta.27", + "xterm-addon-canvas": "0.6.0-beta.27", "xterm-addon-image": "0.6.0-beta.21", - "xterm-addon-search": "0.14.0-beta.18", - "xterm-addon-serialize": "0.12.0-beta.18", - "xterm-addon-unicode11": "0.7.0-beta.18", - "xterm-addon-webgl": "0.17.0-beta.18", - "xterm-headless": "5.4.0-beta.19", + "xterm-addon-search": "0.14.0-beta.26", + "xterm-addon-serialize": "0.12.0-beta.26", + "xterm-addon-unicode11": "0.7.0-beta.26", + "xterm-addon-webgl": "0.17.0-beta.26", + "xterm-headless": "5.4.0-beta.27", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/package.json b/remote/package.json index 5da071b0724..3b74941c201 100644 --- a/remote/package.json +++ b/remote/package.json @@ -26,14 +26,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.4.0-beta.19", - "xterm-addon-canvas": "0.6.0-beta.19", + "xterm": "5.4.0-beta.27", + "xterm-addon-canvas": "0.6.0-beta.27", "xterm-addon-image": "0.6.0-beta.21", - "xterm-addon-search": "0.14.0-beta.18", - "xterm-addon-serialize": "0.12.0-beta.18", - "xterm-addon-unicode11": "0.7.0-beta.18", - "xterm-addon-webgl": "0.17.0-beta.18", - "xterm-headless": "5.4.0-beta.19", + "xterm-addon-search": "0.14.0-beta.26", + "xterm-addon-serialize": "0.12.0-beta.26", + "xterm-addon-unicode11": "0.7.0-beta.26", + "xterm-addon-webgl": "0.17.0-beta.26", + "xterm-headless": "5.4.0-beta.27", "yauzl": "^2.9.2", "yazl": "^2.4.3" } diff --git a/remote/web/package.json b/remote/web/package.json index 6a4f9fdd64b..c06fcbff6cc 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,11 +11,11 @@ "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", "vscode-textmate": "9.0.0", - "xterm": "5.4.0-beta.19", - "xterm-addon-canvas": "0.6.0-beta.19", + "xterm": "5.4.0-beta.27", + "xterm-addon-canvas": "0.6.0-beta.27", "xterm-addon-image": "0.6.0-beta.21", - "xterm-addon-search": "0.14.0-beta.18", - "xterm-addon-unicode11": "0.7.0-beta.18", - "xterm-addon-webgl": "0.17.0-beta.18" + "xterm-addon-search": "0.14.0-beta.26", + "xterm-addon-unicode11": "0.7.0-beta.26", + "xterm-addon-webgl": "0.17.0-beta.26" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index f79ce7f8f9c..91d7cdfb083 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -68,32 +68,32 @@ vscode-textmate@9.0.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-9.0.0.tgz#313c6c8792b0507aef35aeb81b6b370b37c44d6c" integrity sha512-Cl65diFGxz7gpwbav10HqiY/eVYTO1sjQpmRmV991Bj7wAoOAjGQ97PpQcXorDE2Uc4hnGWLY17xme+5t6MlSg== -xterm-addon-canvas@0.6.0-beta.19: - version "0.6.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.19.tgz#c9e01330a548fbb243d731728e70ae77e6dc8c4f" - integrity sha512-S2tZXqnAqkLA5r40gbOlciR7CLtfztn0lk/ko8Bol08pgSqEzWKF0yadYpsezHRmemvTSmyePCtOTqDrEgFQBA== +xterm-addon-canvas@0.6.0-beta.27: + version "0.6.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.27.tgz#2517f050d165b093a3c3e564e4420ccc3ccbad75" + integrity sha512-mSxEJKPnXYKkD6/zQLdNH6kB+sr4B+4DMFzntWgxLjHJdyOO95wUSAtBFnhAUez2nNYvXbs/OXpEbdVdO7f2kQ== xterm-addon-image@0.6.0-beta.21: version "0.6.0-beta.21" resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.21.tgz#e3708bc504c56a23ff31f12a2eeb335331a92aac" integrity sha512-8/PTaXVPa4kQ0xzVeuZZk10OpbZBj2cgfwhM2B0ChSPvwrk0lX+ksnXdtDKH3tg+JYvo7fIhNXtkr4NwWt7VJQ== -xterm-addon-search@0.14.0-beta.18: - version "0.14.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.18.tgz#6298b34d9c590f3e3acdd101eb297eaf8cb1f298" - integrity sha512-1CF2bPz9/vQR+q7OFgjvbBRQ0rUSkiKlwZJMnizgbKl6qx0GNg15T52J+l8zLgg8HavlF8aVps1co1A+N5PPZA== +xterm-addon-search@0.14.0-beta.26: + version "0.14.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.26.tgz#2b5b8af31613c896d354c4799624090b11cd601c" + integrity sha512-CghsGO7fJa0efClbgZH20lh/JUaQYgJ1AJTPm8luc/eDc6DWOJblU0MxIABclLgT8lagv9+sOQfO0VIkAITxig== -xterm-addon-unicode11@0.7.0-beta.18: - version "0.7.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.18.tgz#8e47b5be3ce5f07a136ca66919f548680da96648" - integrity sha512-5Zy2Kn7kSYQP2ItPV9HNsX2u6/XajB6uyRZ/tx5U79XjZIOMMtPLki56fiGxBOlyhIHFr96bwRlvYKZcEY1ndQ== +xterm-addon-unicode11@0.7.0-beta.26: + version "0.7.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.26.tgz#f9606231a8f13e57dbdec5e884b044b0813931f5" + integrity sha512-po+z1ayyrkWh8IGXKpbwCLKLKfcjotZVKqowU6PtHuDtJm/J8rlzvV2eJU1WQ/8ezpopU09ibWCvaf1a7EPuxA== -xterm-addon-webgl@0.17.0-beta.18: - version "0.17.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.18.tgz#0ce7b2ed4f1aaefff0eb59dac5c0e2fbba08d9ec" - integrity sha512-F94/+Koo98fAwVr8zFw4vYnmZPKyN6K4ZQTDM2ICozBHtiVWgR2PjhBP8covD6vXwbsnrwq5aipJLbhBMeZ60w== +xterm-addon-webgl@0.17.0-beta.26: + version "0.17.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.26.tgz#aee4a043981d5d303b7112ef7049bc2865e75393" + integrity sha512-N8CuAPZnoDlQ6yV7n4eXQ2ONPr/GdxiwgxrJjNks4CzzHiJREm23FQIv0fCTwKQS5xU3qoc4LlT3vZ1tKGjtQw== -xterm@5.4.0-beta.19: - version "5.4.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.19.tgz#5177e37c8e885aa5cbbb0b1972e9df7634a8aa33" - integrity sha512-eM5UmMf3ml8NIBixEwH5CKj5rgwiZhE51W8xhs7js1GIGVusG/1SL7gS6d/n9UlspnAvQtUOIqzc70x887m6jQ== +xterm@5.4.0-beta.27: + version "5.4.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.27.tgz#f641ee045a65c9c8967fac534a202062706a8fa9" + integrity sha512-gKqtrjy0RLk2123oFyPw5tkV96jGz4c/JkY8/XUvBXoMVsX4A7rVKpHlmHhmnuK1X5ERAkvCD21YE7LfB8WYkw== diff --git a/remote/yarn.lock b/remote/yarn.lock index 21b77b54043..27f1fd84afb 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -667,45 +667,45 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-canvas@0.6.0-beta.19: - version "0.6.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.19.tgz#c9e01330a548fbb243d731728e70ae77e6dc8c4f" - integrity sha512-S2tZXqnAqkLA5r40gbOlciR7CLtfztn0lk/ko8Bol08pgSqEzWKF0yadYpsezHRmemvTSmyePCtOTqDrEgFQBA== +xterm-addon-canvas@0.6.0-beta.27: + version "0.6.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.27.tgz#2517f050d165b093a3c3e564e4420ccc3ccbad75" + integrity sha512-mSxEJKPnXYKkD6/zQLdNH6kB+sr4B+4DMFzntWgxLjHJdyOO95wUSAtBFnhAUez2nNYvXbs/OXpEbdVdO7f2kQ== xterm-addon-image@0.6.0-beta.21: version "0.6.0-beta.21" resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.21.tgz#e3708bc504c56a23ff31f12a2eeb335331a92aac" integrity sha512-8/PTaXVPa4kQ0xzVeuZZk10OpbZBj2cgfwhM2B0ChSPvwrk0lX+ksnXdtDKH3tg+JYvo7fIhNXtkr4NwWt7VJQ== -xterm-addon-search@0.14.0-beta.18: - version "0.14.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.18.tgz#6298b34d9c590f3e3acdd101eb297eaf8cb1f298" - integrity sha512-1CF2bPz9/vQR+q7OFgjvbBRQ0rUSkiKlwZJMnizgbKl6qx0GNg15T52J+l8zLgg8HavlF8aVps1co1A+N5PPZA== +xterm-addon-search@0.14.0-beta.26: + version "0.14.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.26.tgz#2b5b8af31613c896d354c4799624090b11cd601c" + integrity sha512-CghsGO7fJa0efClbgZH20lh/JUaQYgJ1AJTPm8luc/eDc6DWOJblU0MxIABclLgT8lagv9+sOQfO0VIkAITxig== -xterm-addon-serialize@0.12.0-beta.18: - version "0.12.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.12.0-beta.18.tgz#0d9369acb49fa01f124dfff064a17f4874211f1a" - integrity sha512-RyV6iU/KRC3QN29i3iaWzm33ACbi0gMQW+LjKSFJN/XrNO1QTqfnh2VZp58G32IFG8l2sg/FUs2gXtEB5IMlhA== +xterm-addon-serialize@0.12.0-beta.26: + version "0.12.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.12.0-beta.26.tgz#cb5bd80128e82880369cb012938e14414b182aa1" + integrity sha512-b4lOcttE6lqAF3zB2l8XtDShe5djhl9SueljnVWuG4mYMYPQoiklxFcpY66sjSCIAS6NsbtrL/LGQ/0eZGi+Ig== -xterm-addon-unicode11@0.7.0-beta.18: - version "0.7.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.18.tgz#8e47b5be3ce5f07a136ca66919f548680da96648" - integrity sha512-5Zy2Kn7kSYQP2ItPV9HNsX2u6/XajB6uyRZ/tx5U79XjZIOMMtPLki56fiGxBOlyhIHFr96bwRlvYKZcEY1ndQ== +xterm-addon-unicode11@0.7.0-beta.26: + version "0.7.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.26.tgz#f9606231a8f13e57dbdec5e884b044b0813931f5" + integrity sha512-po+z1ayyrkWh8IGXKpbwCLKLKfcjotZVKqowU6PtHuDtJm/J8rlzvV2eJU1WQ/8ezpopU09ibWCvaf1a7EPuxA== -xterm-addon-webgl@0.17.0-beta.18: - version "0.17.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.18.tgz#0ce7b2ed4f1aaefff0eb59dac5c0e2fbba08d9ec" - integrity sha512-F94/+Koo98fAwVr8zFw4vYnmZPKyN6K4ZQTDM2ICozBHtiVWgR2PjhBP8covD6vXwbsnrwq5aipJLbhBMeZ60w== +xterm-addon-webgl@0.17.0-beta.26: + version "0.17.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.26.tgz#aee4a043981d5d303b7112ef7049bc2865e75393" + integrity sha512-N8CuAPZnoDlQ6yV7n4eXQ2ONPr/GdxiwgxrJjNks4CzzHiJREm23FQIv0fCTwKQS5xU3qoc4LlT3vZ1tKGjtQw== -xterm-headless@5.4.0-beta.19: - version "5.4.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.4.0-beta.19.tgz#cdbad09917bdbeeae9197c87663d603fc2794212" - integrity sha512-lLHbZ0DUBoolt4kWCchBAZDlDDdWROwIFxzG8sK389/Z7AlVWsA7kasz2TIPN+l9SC7MrhDdWIFwi1Z0eVODcg== +xterm-headless@5.4.0-beta.27: + version "5.4.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.4.0-beta.27.tgz#cfce5f86e83580388238ea204bb451b7ffe94dc9" + integrity sha512-vdrq5eeNMyHZRDw5XR/TPl8oPln0BqbR07akt/fDXMsVg6YwWG+UOnU6GIMj7bJaBed5YkPV9NeBtdsVQn4Lyw== -xterm@5.4.0-beta.19: - version "5.4.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.19.tgz#5177e37c8e885aa5cbbb0b1972e9df7634a8aa33" - integrity sha512-eM5UmMf3ml8NIBixEwH5CKj5rgwiZhE51W8xhs7js1GIGVusG/1SL7gS6d/n9UlspnAvQtUOIqzc70x887m6jQ== +xterm@5.4.0-beta.27: + version "5.4.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.27.tgz#f641ee045a65c9c8967fac534a202062706a8fa9" + integrity sha512-gKqtrjy0RLk2123oFyPw5tkV96jGz4c/JkY8/XUvBXoMVsX4A7rVKpHlmHhmnuK1X5ERAkvCD21YE7LfB8WYkw== yallist@^4.0.0: version "4.0.0" diff --git a/src/vs/base/browser/ui/icons/iconSelectBox.css b/src/vs/base/browser/ui/icons/iconSelectBox.css index 4e2278d04bc..399d5779423 100644 --- a/src/vs/base/browser/ui/icons/iconSelectBox.css +++ b/src/vs/base/browser/ui/icons/iconSelectBox.css @@ -35,4 +35,5 @@ .icon-select-box .icon-select-id-container .icon-select-id-label .highlight { color: var(--vscode-list-highlightForeground); + font-weight: bold; } diff --git a/src/vs/base/browser/ui/icons/iconSelectBox.ts b/src/vs/base/browser/ui/icons/iconSelectBox.ts index c592c37d3f7..a39532699b2 100644 --- a/src/vs/base/browser/ui/icons/iconSelectBox.ts +++ b/src/vs/base/browser/ui/icons/iconSelectBox.ts @@ -19,6 +19,7 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte export interface IIconSelectBoxOptions { readonly icons: ThemeIcon[]; readonly inputBoxStyles: IInputBoxStyles; + readonly showIconInfo?: boolean; } interface IRenderedIconItem { @@ -46,7 +47,7 @@ export class IconSelectBox extends Disposable { private scrollableElement: DomScrollableElement | undefined; private iconIdElement: HighlightedLabel | undefined; private readonly iconContainerWidth = 36; - private readonly iconContainerHeight = 32; + private readonly iconContainerHeight = 36; constructor( private readonly options: IIconSelectBoxOptions, @@ -78,7 +79,10 @@ export class IconSelectBox extends Disposable { horizontal: ScrollbarVisibility.Hidden, })); dom.append(iconSelectBoxContainer, this.scrollableElement.getDomNode()); - this.iconIdElement = new HighlightedLabel(dom.append(dom.append(iconSelectBoxContainer, dom.$('.icon-select-id-container')), dom.$('.icon-select-id-label'))); + + if (this.options.showIconInfo) { + this.iconIdElement = new HighlightedLabel(dom.append(dom.append(iconSelectBoxContainer, dom.$('.icon-select-id-container')), dom.$('.icon-select-id-label'))); + } const iconsDisposables = disposables.add(new MutableDisposable()); iconsDisposables.value = this.renderIcons(this.options.icons, [], iconsContainer); @@ -227,7 +231,7 @@ export class IconSelectBox extends Disposable { } if (this.scrollableElement) { - this.scrollableElement.getDomNode().style.height = `${dimension.height - 80}px`; + this.scrollableElement.getDomNode().style.height = `${this.iconIdElement ? dimension.height - 80 : dimension.height - 40}px`; this.scrollableElement.scanDomNode(); } } @@ -244,6 +248,12 @@ export class IconSelectBox extends Disposable { this._onDidSelect.fire(this.renderedIcons[index].icon); } + clearInput(): void { + if (this.inputBox) { + this.inputBox.value = ''; + } + } + focus(): void { this.inputBox?.focus(); this.focusIcon(0); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 8ce413f6e70..98b6aff2b4e 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -6,7 +6,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableMap, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { extUri as defaultExtUri, IExtUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { setTimeout0 } from 'vs/base/common/platform'; @@ -490,9 +490,36 @@ export function timeout(millis: number, token?: CancellationToken): CancelablePr }); } -export function disposableTimeout(handler: () => void, timeout = 0): IDisposable { - const timer = setTimeout(handler, timeout); - return toDisposable(() => clearTimeout(timer)); +/** + * Creates a timeout that can be disposed using its returned value. + * @param handler The timeout handler. + * @param timeout An optional timeout in milliseconds. + * @param store An optional {@link DisposableStore} that will have the timeout disposable managed automatically. + * + * @example + * const store = new DisposableStore; + * // Call the timeout after 1000ms at which point it will be automatically + * // evicted from the store. + * const timeoutDisposable = disposableTimeout(() => {}, 1000, store); + * + * if (foo) { + * // Cancel the timeout and evict it from store. + * timeoutDisposable.dispose(); + * } + */ +export function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable { + const timer = setTimeout(() => { + handler(); + if (store) { + disposable.dispose(); + } + }, timeout); + const disposable = toDisposable(() => { + clearTimeout(timer); + store?.deleteAndLeak(disposable); + }); + store?.add(disposable); + return disposable; } /** diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 0afa0bbd79b..de7b5d15e6e 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -430,6 +430,34 @@ export class DisposableStore implements IDisposable { return o; } + + /** + * Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the + * disposable even when the disposable is not part in the store. + */ + public delete(o: T): void { + if (!o) { + return; + } + if ((o as unknown as DisposableStore) === this) { + throw new Error('Cannot dispose a disposable on itself!'); + } + this._toDispose.delete(o); + o.dispose(); + } + + /** + * Deletes the value from the store, but does not dispose it. + */ + public deleteAndLeak(o: T): void { + if (!o) { + return; + } + if (this._toDispose.has(o)) { + this._toDispose.delete(o); + setParentOfDisposable(o, null); + } + } } /** diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 4cb9eceb360..bd6ec6a4bcc 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -12,6 +12,7 @@ import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Async', () => { @@ -807,6 +808,65 @@ suite('Async', () => { }); }); + suite('disposableTimeout', () => { + test('handler only success', async () => { + let cb = false; + const t = async.disposableTimeout(() => cb = true); + + await async.timeout(0); + + assert.strictEqual(cb, true); + + t.dispose(); + }); + + test('handler only cancel', async () => { + let cb = false; + const t = async.disposableTimeout(() => cb = true); + t.dispose(); + + await async.timeout(0); + + assert.strictEqual(cb, false); + }); + + test('store managed success', async () => { + let cb = false; + const s = new DisposableStore(); + async.disposableTimeout(() => cb = true, 0, s); + + await async.timeout(0); + + assert.strictEqual(cb, true); + + s.dispose(); + }); + + test('store managed cancel via disposable', async () => { + let cb = false; + const s = new DisposableStore(); + const t = async.disposableTimeout(() => cb = true, 0, s); + t.dispose(); + + await async.timeout(0); + + assert.strictEqual(cb, false); + + s.dispose(); + }); + + test('store managed cancel via store', async () => { + let cb = false; + const s = new DisposableStore(); + async.disposableTimeout(() => cb = true, 0, s); + s.dispose(); + + await async.timeout(0); + + assert.strictEqual(cb, false); + }); + }); + test('raceCancellation', async () => { const cts = store.add(new CancellationTokenSource()); const ctsTimeout = store.add(new CancellationTokenSource()); diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index ae8627eb117..22e8ff77e0f 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -172,6 +172,52 @@ suite('DisposableStore', () => { assert.strictEqual((thrownError as AggregateError).errors[0].message, 'I am error 1'); assert.strictEqual((thrownError as AggregateError).errors[1].message, 'I am error 2'); }); + + test('delete should evict and dispose of the disposables', () => { + const disposedValues = new Set(); + const disposables: IDisposable[] = [ + toDisposable(() => { disposedValues.add(1); }), + toDisposable(() => { disposedValues.add(2); }) + ]; + + const store = new DisposableStore(); + store.add(disposables[0]); + store.add(disposables[1]); + + store.delete(disposables[0]); + + assert.ok(disposedValues.has(1)); + assert.ok(!disposedValues.has(2)); + + store.dispose(); + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(2)); + }); + + test('deleteAndLeak should evict and not dispose of the disposables', () => { + const disposedValues = new Set(); + const disposables: IDisposable[] = [ + toDisposable(() => { disposedValues.add(1); }), + toDisposable(() => { disposedValues.add(2); }) + ]; + + const store = new DisposableStore(); + store.add(disposables[0]); + store.add(disposables[1]); + + store.deleteAndLeak(disposables[0]); + + assert.ok(!disposedValues.has(1)); + assert.ok(!disposedValues.has(2)); + + store.dispose(); + + assert.ok(!disposedValues.has(1)); + assert.ok(disposedValues.has(2)); + + disposables[0].dispose(); + }); }); suite('Reference Collection', () => { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 08ad5897812..754f4a1f87e 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -448,8 +448,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi })), token); const edits = coalesce(results ?? []); - sortEditsByYieldTo(edits); - return edits; + return sortEditsByYieldTo(edits); } private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken) { diff --git a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts index cdc830799ef..8a22c84ed88 100644 --- a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts @@ -9,6 +9,7 @@ import { HiddenRangeModel } from 'vs/editor/contrib/folding/browser/hiddenRangeM import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { TestDecorationProvider } from './foldingModel.test'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface ExpectedRange { @@ -41,59 +42,60 @@ suite('Hidden Range Model', () => { const textModel = createTextModel(lines.join('\n')); const foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel)); const hiddenRangeModel = new HiddenRangeModel(foldingModel); + try { + assert.strictEqual(hiddenRangeModel.hasRanges(), false); - assert.strictEqual(hiddenRangeModel.hasRanges(), false); + const ranges = computeRanges(textModel, false, undefined); + foldingModel.update(ranges); - const ranges = computeRanges(textModel, false, undefined); - foldingModel.update(ranges); + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(6)!]); + assertRanges(hiddenRangeModel.hiddenRanges, [r(2, 3), r(7, 7)]); - foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(6)!]); - assertRanges(hiddenRangeModel.hiddenRanges, [r(2, 3), r(7, 7)]); + assert.strictEqual(hiddenRangeModel.hasRanges(), true); + assert.strictEqual(hiddenRangeModel.isHidden(1), false); + assert.strictEqual(hiddenRangeModel.isHidden(2), true); + assert.strictEqual(hiddenRangeModel.isHidden(3), true); + assert.strictEqual(hiddenRangeModel.isHidden(4), false); + assert.strictEqual(hiddenRangeModel.isHidden(5), false); + assert.strictEqual(hiddenRangeModel.isHidden(6), false); + assert.strictEqual(hiddenRangeModel.isHidden(7), true); + assert.strictEqual(hiddenRangeModel.isHidden(8), false); + assert.strictEqual(hiddenRangeModel.isHidden(9), false); + assert.strictEqual(hiddenRangeModel.isHidden(10), false); - assert.strictEqual(hiddenRangeModel.hasRanges(), true); - assert.strictEqual(hiddenRangeModel.isHidden(1), false); - assert.strictEqual(hiddenRangeModel.isHidden(2), true); - assert.strictEqual(hiddenRangeModel.isHidden(3), true); - assert.strictEqual(hiddenRangeModel.isHidden(4), false); - assert.strictEqual(hiddenRangeModel.isHidden(5), false); - assert.strictEqual(hiddenRangeModel.isHidden(6), false); - assert.strictEqual(hiddenRangeModel.isHidden(7), true); - assert.strictEqual(hiddenRangeModel.isHidden(8), false); - assert.strictEqual(hiddenRangeModel.isHidden(9), false); - assert.strictEqual(hiddenRangeModel.isHidden(10), false); + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(4)!]); + assertRanges(hiddenRangeModel.hiddenRanges, [r(2, 3), r(5, 9)]); - foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(4)!]); - assertRanges(hiddenRangeModel.hiddenRanges, [r(2, 3), r(5, 9)]); - - assert.strictEqual(hiddenRangeModel.hasRanges(), true); - assert.strictEqual(hiddenRangeModel.isHidden(1), false); - assert.strictEqual(hiddenRangeModel.isHidden(2), true); - assert.strictEqual(hiddenRangeModel.isHidden(3), true); - assert.strictEqual(hiddenRangeModel.isHidden(4), false); - assert.strictEqual(hiddenRangeModel.isHidden(5), true); - assert.strictEqual(hiddenRangeModel.isHidden(6), true); - assert.strictEqual(hiddenRangeModel.isHidden(7), true); - assert.strictEqual(hiddenRangeModel.isHidden(8), true); - assert.strictEqual(hiddenRangeModel.isHidden(9), true); - assert.strictEqual(hiddenRangeModel.isHidden(10), false); - - foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(6)!, foldingModel.getRegionAtLine(4)!]); - assertRanges(hiddenRangeModel.hiddenRanges, []); - assert.strictEqual(hiddenRangeModel.hasRanges(), false); - assert.strictEqual(hiddenRangeModel.isHidden(1), false); - assert.strictEqual(hiddenRangeModel.isHidden(2), false); - assert.strictEqual(hiddenRangeModel.isHidden(3), false); - assert.strictEqual(hiddenRangeModel.isHidden(4), false); - assert.strictEqual(hiddenRangeModel.isHidden(5), false); - assert.strictEqual(hiddenRangeModel.isHidden(6), false); - assert.strictEqual(hiddenRangeModel.isHidden(7), false); - assert.strictEqual(hiddenRangeModel.isHidden(8), false); - assert.strictEqual(hiddenRangeModel.isHidden(9), false); - assert.strictEqual(hiddenRangeModel.isHidden(10), false); - - textModel.dispose(); + assert.strictEqual(hiddenRangeModel.hasRanges(), true); + assert.strictEqual(hiddenRangeModel.isHidden(1), false); + assert.strictEqual(hiddenRangeModel.isHidden(2), true); + assert.strictEqual(hiddenRangeModel.isHidden(3), true); + assert.strictEqual(hiddenRangeModel.isHidden(4), false); + assert.strictEqual(hiddenRangeModel.isHidden(5), true); + assert.strictEqual(hiddenRangeModel.isHidden(6), true); + assert.strictEqual(hiddenRangeModel.isHidden(7), true); + assert.strictEqual(hiddenRangeModel.isHidden(8), true); + assert.strictEqual(hiddenRangeModel.isHidden(9), true); + assert.strictEqual(hiddenRangeModel.isHidden(10), false); + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(6)!, foldingModel.getRegionAtLine(4)!]); + assertRanges(hiddenRangeModel.hiddenRanges, []); + assert.strictEqual(hiddenRangeModel.hasRanges(), false); + assert.strictEqual(hiddenRangeModel.isHidden(1), false); + assert.strictEqual(hiddenRangeModel.isHidden(2), false); + assert.strictEqual(hiddenRangeModel.isHidden(3), false); + assert.strictEqual(hiddenRangeModel.isHidden(4), false); + assert.strictEqual(hiddenRangeModel.isHidden(5), false); + assert.strictEqual(hiddenRangeModel.isHidden(6), false); + assert.strictEqual(hiddenRangeModel.isHidden(7), false); + assert.strictEqual(hiddenRangeModel.isHidden(8), false); + assert.strictEqual(hiddenRangeModel.isHidden(9), false); + assert.strictEqual(hiddenRangeModel.isHidden(10), false); + } finally { + textModel.dispose(); + hiddenRangeModel.dispose(); + } }); - + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts index 72f6046db88..59e4e0723dd 100644 --- a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts @@ -9,6 +9,7 @@ import { FoldingContext, FoldingRange, FoldingRangeProvider, ProviderResult } fr import { SyntaxRangeProvider } from 'vs/editor/contrib/folding/browser/syntaxRangeProvider'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { FoldingLimitReporter } from 'vs/editor/contrib/folding/browser/folding'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface IndentRange { start: number; @@ -77,15 +78,20 @@ suite('Syntax folding', () => { async function assertLimit(maxEntries: number, expectedRanges: IndentRange[], message: string) { let reported: number | false = false; const foldingRangesLimit: FoldingLimitReporter = { limit: maxEntries, update: (computed, limited) => reported = limited }; - const indentRanges = await new SyntaxRangeProvider(model, providers, () => { }, foldingRangesLimit, undefined).compute(CancellationToken.None); - const actual: IndentRange[] = []; - if (indentRanges) { - for (let i = 0; i < indentRanges.length; i++) { - actual.push({ start: indentRanges.getStartLineNumber(i), end: indentRanges.getEndLineNumber(i) }); + const syntaxRangeProvider = new SyntaxRangeProvider(model, providers, () => { }, foldingRangesLimit, undefined); + try { + const indentRanges = await syntaxRangeProvider.compute(CancellationToken.None); + const actual: IndentRange[] = []; + if (indentRanges) { + for (let i = 0; i < indentRanges.length; i++) { + actual.push({ start: indentRanges.getStartLineNumber(i), end: indentRanges.getEndLineNumber(i) }); + } + assert.equal(reported, 9 <= maxEntries ? false : maxEntries, 'limited'); } - assert.equal(reported, 9 <= maxEntries ? false : maxEntries, 'limited'); + assert.deepStrictEqual(actual, expectedRanges, message); + } finally { + syntaxRangeProvider.dispose(); } - assert.deepStrictEqual(actual, expectedRanges, message); } @@ -103,5 +109,5 @@ suite('Syntax folding', () => { model.dispose(); }); - + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index 939412a805e..da07ede149f 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -6,7 +6,7 @@ import { app, Event as ElectronEvent } from 'electron'; import { disposableTimeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -26,12 +26,10 @@ import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; * that calls VSCode with the `open-url` command line argument * (https://github.com/microsoft/vscode/pull/56727) */ -export class ElectronURLListener { +export class ElectronURLListener extends Disposable { private uris: IProtocolUrl[] = []; private retryCount = 0; - private flushDisposable: IDisposable = Disposable.None; - private readonly disposables = new DisposableStore(); constructor( initialProtocolUrls: IProtocolUrl[] | undefined, @@ -41,6 +39,8 @@ export class ElectronURLListener { productService: IProductService, private readonly logService: ILogService ) { + super(); + if (initialProtocolUrls) { logService.trace('ElectronURLListener initialUrisToHandle:', initialProtocolUrls.map(url => url.originalUrl)); @@ -64,7 +64,7 @@ export class ElectronURLListener { return url; }); - this.disposables.add(onOpenElectronUrl(url => { + this._register(onOpenElectronUrl(url => { const uri = this.uriFromRawUrl(url); if (!uri) { return; @@ -85,7 +85,7 @@ export class ElectronURLListener { } else { logService.trace('ElectronURLListener: waiting for window to be ready to handle URLs...'); - Event.once(windowsMainService.onDidSignalReadyWindow)(this.flush, this, this.disposables); + this._register(Event.once(windowsMainService.onDidSignalReadyWindow)(this.flush)); } } @@ -124,11 +124,6 @@ export class ElectronURLListener { } this.uris = uris; - this.flushDisposable = disposableTimeout(() => this.flush(), 500); - } - - dispose(): void { - this.disposables.dispose(); - this.flushDisposable.dispose(); + disposableTimeout(() => this.flush(), 500, this._store); } } diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index e42d012c995..f18de2499cd 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -41,6 +41,7 @@ export interface IUserDataProfile { readonly isDefault: boolean; readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly location: URI; readonly globalStorageHome: URI; readonly settingsResource: URI; @@ -84,12 +85,14 @@ export type WillRemoveProfileEvent = { export interface IUserDataProfileOptions { readonly shortName?: string; + readonly icon?: string; readonly useDefaultFlags?: UseDefaultProfileFlags; readonly transient?: boolean; } -export interface IUserDataProfileUpdateOptions extends IUserDataProfileOptions { +export interface IUserDataProfileUpdateOptions extends Omit { readonly name?: string; + readonly icon?: string | null; } export const IUserDataProfilesService = createDecorator('IUserDataProfilesService'); @@ -124,6 +127,7 @@ export function reviveProfile(profile: UriDto, scheme: string) isDefault: profile.isDefault, name: profile.name, shortName: profile.shortName, + icon: profile.icon, location: URI.revive(profile.location).with({ scheme }), globalStorageHome: URI.revive(profile.globalStorageHome).with({ scheme }), settingsResource: URI.revive(profile.settingsResource).with({ scheme }), @@ -144,6 +148,7 @@ export function toUserDataProfile(id: string, name: string, location: URI, profi location, isDefault: false, shortName: options?.shortName, + icon: options?.icon, globalStorageHome: defaultProfile && options?.useDefaultFlags?.globalState ? defaultProfile.globalStorageHome : joinPath(location, 'globalStorage'), settingsResource: defaultProfile && options?.useDefaultFlags?.settings ? defaultProfile.settingsResource : joinPath(location, 'settings.json'), keybindingsResource: defaultProfile && options?.useDefaultFlags?.keybindings ? defaultProfile.keybindingsResource : joinPath(location, 'keybindings.json'), @@ -166,6 +171,7 @@ export type StoredUserDataProfile = { name: string; location: URI; shortName?: string; + icon?: string; useDefaultFlags?: UseDefaultProfileFlags; }; @@ -246,7 +252,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf this.logService.warn('Skipping the invalid stored profile', storedProfile.location || storedProfile.name); continue; } - profiles.push(toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, this.profilesCacheHome, { shortName: storedProfile.shortName, useDefaultFlags: storedProfile.useDefaultFlags }, defaultProfile)); + profiles.push(toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, this.profilesCacheHome, { shortName: storedProfile.shortName, icon: storedProfile.icon, useDefaultFlags: storedProfile.useDefaultFlags }, defaultProfile)); } } catch (error) { this.logService.error(error); @@ -365,7 +371,12 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf throw new Error(`Profile '${profileToUpdate.name}' does not exist`); } - profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, this.profilesCacheHome, { shortName: options.shortName ?? profile.shortName, transient: options.transient ?? profile.isTransient, useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags }, this.defaultProfile); + profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, this.profilesCacheHome, { + shortName: options.shortName ?? profile.shortName, + icon: options.icon === null ? undefined : options.icon ?? profile.icon, + transient: options.transient ?? profile.isTransient, + useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags + }, this.defaultProfile); this.updateProfiles([], [], [profile]); return profile; @@ -516,7 +527,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf if (profile.isTransient) { this.transientProfilesObject.profiles.push(profile); } else { - storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } this.saveStoredProfiles(storedProfiles); diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts index 072554b04f8..a6957351fc5 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts @@ -18,6 +18,7 @@ interface IUserDataProfileInfo { readonly id: string; readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly useDefaultFlags?: UseDefaultProfileFlags; } @@ -119,7 +120,7 @@ function compare(from: IUserDataProfileInfo[] | null, to: IUserDataProfileInfo[] const removed = fromKeys.filter(key => !toKeys.includes(key)); const updated: string[] = []; - for (const { id, name, shortName, useDefaultFlags } of from) { + for (const { id, name, shortName, icon, useDefaultFlags } of from) { if (removed.includes(id)) { continue; } @@ -127,6 +128,7 @@ function compare(from: IUserDataProfileInfo[] | null, to: IUserDataProfileInfo[] if (!toProfile || toProfile.name !== name || toProfile.shortName !== shortName + || toProfile.icon !== icon || !equals(toProfile.useDefaultFlags, useDefaultFlags) ) { updated.push(id); diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index eee01fe850c..5a532c8bae2 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -191,7 +191,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of local.added) { promises.push((async () => { this.logService.trace(`${this.syncResourceLogLabel}: Creating '${profile.name}' profile...`); - await this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + await this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); this.logService.info(`${this.syncResourceLogLabel}: Created profile '${profile.name}'.`); })()); } @@ -207,7 +207,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i if (localProfile) { promises.push((async () => { this.logService.trace(`${this.syncResourceLogLabel}: Updating '${profile.name}' profile...`); - await this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + await this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); this.logService.info(`${this.syncResourceLogLabel}: Updated profile '${profile.name}'.`); })()); } else { @@ -225,7 +225,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of remote?.added || []) { const collection = await this.userDataSyncStoreService.createCollection(this.syncHeaders); addedCollections.push(collection); - remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } else { this.logService.info(`${this.syncResourceLogLabel}: Could not create remote profiles as there are too many profiles.`); @@ -236,7 +236,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of remote?.updated || []) { const profileToBeUpdated = remoteProfiles.find(({ id }) => profile.id === id); if (profileToBeUpdated) { - remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, { ...profileToBeUpdated, id: profile.id, name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, { ...profileToBeUpdated, id: profile.id, name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 13f99fc4aa5..084755420cb 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -342,6 +342,7 @@ export interface ISyncUserDataProfile { readonly collection: string; readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly useDefaultFlags?: UseDefaultProfileFlags; } diff --git a/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts b/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts index e31e85e0e57..d68b3ee4103 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts @@ -404,7 +404,6 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc } paths.push(`syncResource:${syncResourceUriInfo.syncResource}`); paths.push(`profile:${syncResourceUriInfo.profile}`); - paths.push(syncResourceUriInfo.profile); if (syncResourceUriInfo.collection) { paths.push(`collection:${syncResourceUriInfo.collection}`); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 03550d2d646..e5b880451cb 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -34,6 +34,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; //#region Editor / Resources DND @@ -95,7 +96,7 @@ export class ResourcesDropHandler { ) { } - async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise { + async handleDrop(event: DragEvent, resolveTargetGroup?: () => IEditorGroup | undefined, afterDrop?: (targetGroup: IEditorGroup | undefined) => void, options?: IEditorOptions): Promise { const editors = await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, event)); if (!editors.length) { return; @@ -122,19 +123,19 @@ export class ResourcesDropHandler { } // Open in Editor - const targetGroup = resolveTargetGroup(); + const targetGroup = resolveTargetGroup?.(); await this.editorService.openEditors(editors.map(editor => ({ ...editor, resource: editor.resource, options: { ...editor.options, - pinned: true, - index: targetIndex + ...options, + pinned: true } })), targetGroup, { validateTrust: true }); // Finish with provided function - afterDrop(targetGroup); + afterDrop?.(targetGroup); } private async handleWorkspaceDrop(resources: URI[]): Promise { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 6e8afba3bf5..3fc1bdc4e82 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -492,7 +492,6 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { @IKeybindingService keybindingService: IKeybindingService, ) { super(MenuId.GlobalActivity, action, contextMenuActionsProvider, true, colors, activityHoverOptions, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.updateProfileBadge())); } override render(container: HTMLElement): void { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index e9aca0b7601..007ef5fc790 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -43,6 +43,8 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { GestureEvent } from 'vs/base/browser/touch'; import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/browser/parts/paneCompositePart'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; interface IPlaceholderViewContainer { readonly id: string; @@ -130,6 +132,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, ) { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -523,10 +526,11 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart preventLoopNavigation: true })); - this.globalActivityAction = this._register(new ActivityAction({ - id: 'workbench.actions.manage', - name: localize('manage', "Manage"), - classNames: ThemeIcon.asClassNameArray(ActivitybarPart.GEAR_ICON), + this.globalActivityAction = this._register(new ActivityAction(this.createGlobalActivity(this.userDataProfileService.currentProfile))); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => { + if (this.globalActivityAction) { + this.globalActivityAction.activity = this.createGlobalActivity(e.profile); + } })); if (this.accountsVisibilityPreference) { @@ -542,6 +546,14 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.globalActivityActionBar.push(this.globalActivityAction); } + private createGlobalActivity(profile: IUserDataProfile): IActivity { + return { + id: 'workbench.actions.manage', + name: localize('manage', "Manage"), + classNames: ThemeIcon.asClassNameArray(profile.icon ? ThemeIcon.fromId(profile.icon) : ActivitybarPart.GEAR_ICON), + }; + } + private toggleAccountsActivity() { if (!!this.accountsActivityAction === this.accountsVisibilityPreference) { return; diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 8764654ad5a..bbd93707d10 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -31,6 +31,7 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { tabSizingFixedMinWidth: 50, tabSizingFixedMaxWidth: 160, pinnedTabSizing: 'normal', + pinnedTabsOnSeparateRow: false, tabHeight: 'normal', preventPinnedEditorClose: 'keyboardAndMouse', titleScrollbarSizing: 'default', @@ -215,7 +216,7 @@ export interface IInternalEditorCloseOptions extends IInternalEditorTitleControl context?: EditorCloseContext; } -export interface IInternalMoveCopyOptions extends IInternalEditorTitleControlOptions { +export interface IInternalMoveCopyOptions extends IInternalEditorOpenOptions { /** * Whether to close the editor at the source or keep it. diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index f013b140ff4..0d116c056b1 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -198,7 +198,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.element.appendChild(this.titleContainer); // Title control - this.titleControl = this._register(this.scopedInstantiationService.createInstance(EditorTitleControl, this.titleContainer, this.accessor, this)); + this.titleControl = this._register(this.scopedInstantiationService.createInstance(EditorTitleControl, this.titleContainer, this.accessor, this, this.model)); // Editor container this.editorContainer = document.createElement('div'); @@ -685,8 +685,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title control this.titleControl.updateOptions(event.oldPartOptions, event.newPartOptions); - // Title control Switch between showing tabs <=> not showing tabs - if (event.oldPartOptions.showTabs !== event.newPartOptions.showTabs) { + // Title control switch between singleEditorTabs, multiEditorTabs and multiRowEditorTabs + if ( + event.oldPartOptions.showTabs !== event.newPartOptions.showTabs || + (event.oldPartOptions.showTabs && event.oldPartOptions.pinnedTabsOnSeparateRow !== event.newPartOptions.pinnedTabsOnSeparateRow) + ) { // Re-layout this.relayout(); @@ -927,7 +930,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // title control and also make sure to emit this as an event const newIndexOfEditor = this.getIndexOfEditor(editor); if (newIndexOfEditor !== oldIndexOfEditor) { - this.titleControl.moveEditor(editor, oldIndexOfEditor, newIndexOfEditor); + this.titleControl.moveEditor(editor, oldIndexOfEditor, newIndexOfEditor, true); } // Forward sticky state to title control @@ -979,13 +982,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { supportSideBySide: internalOptions?.supportSideBySide }; - if (options?.sticky && typeof options?.index === 'number' && !this.model.isSticky(options.index)) { - // Special case: we are to open an editor sticky but at an index that is not sticky - // In that case we prefer to open the editor at the index but not sticky. This enables - // to drag a sticky editor to an index that is not sticky to unstick it. - openEditorOptions.sticky = false; - } - if (!openEditorOptions.active && !openEditorOptions.pinned && this.model.activeEditor && !this.model.isPinned(this.model.activeEditor)) { // Special case: we are to open an editor inactive and not pinned, but the current active // editor is also not pinned, which means it will get replaced with this one. As such, @@ -1180,7 +1176,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - moveEditor(editor: EditorInput, target: EditorGroupView, options?: IEditorOptions, internalOptions?: IInternalEditorTitleControlOptions): void { + moveEditor(editor: EditorInput, target: EditorGroupView, options?: IEditorOptions, internalOptions?: IInternalMoveCopyOptions): void { // Move within same group if (this === target) { @@ -1199,26 +1195,35 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return; // do nothing if we move into same group without index } - const currentIndex = this.model.indexOf(candidate); - if (currentIndex === -1 || currentIndex === moveToIndex) { - return; // do nothing if editor unknown in model or is already at the given index - } - // Update model and make sure to continue to use the editor we get from // the model. It is possible that the editor was already opened and we // want to ensure that we use the existing instance in that case. + const currentIndex = this.model.indexOf(candidate); const editor = this.model.getEditorByIndex(currentIndex); if (!editor) { return; } - // Update model - this.model.moveEditor(editor, moveToIndex); - this.model.pin(editor); + // Move when index has actually changed + if (currentIndex !== moveToIndex) { + const oldStickyCount = this.model.stickyCount; - // Forward to title control - this.titleControl.moveEditor(editor, currentIndex, moveToIndex); - this.titleControl.pinEditor(editor); + // Update model + this.model.moveEditor(editor, moveToIndex); + this.model.pin(editor); + + // Forward to title control + this.titleControl.moveEditor(editor, currentIndex, moveToIndex, oldStickyCount !== this.model.stickyCount); + this.titleControl.pinEditor(editor); + } + + // Support the option to stick the editor even if it is moved. + // It is important that we call this method after we have moved + // the editor because the result of moving the editor could have + // caused a change in sticky state. + if (options?.sticky) { + this.stickEditor(editor); + } } private doMoveOrCopyEditorAcrossGroups(editor: EditorInput, target: EditorGroupView, openOptions?: IEditorOpenOptions, internalOptions?: IInternalMoveCopyOptions): void { @@ -1229,8 +1234,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // if so const options = fillActiveEditorViewState(this, editor, { ...openOptions, - pinned: true, // always pin moved editor - sticky: !keepCopy && this.model.isSticky(editor) // preserve sticky state only if editor is moved (https://github.com/microsoft/vscode/issues/99035) + pinned: true, // always pin moved editor + sticky: openOptions?.sticky ?? (!keepCopy && this.model.isSticky(editor)) // preserve sticky state only if editor is moved or eplicitly wanted (https://github.com/microsoft/vscode/issues/99035) }); // Indicate will move event diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 67e142dea05..353dc2cc422 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -11,7 +11,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, SubmenuAction, ActionRunner } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -38,6 +38,7 @@ import { LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor/editorTitleControl'; +import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; export interface IToolbarActions { readonly primary: IAction[]; @@ -70,7 +71,25 @@ export class EditorCommandsContextActionRunner extends ActionRunner { } } -export abstract class EditorTabsControl extends Themable { +export interface IEditorTabsControl extends IDisposable { + updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void; + openEditor(editor: EditorInput): boolean; + openEditors(editors: EditorInput[]): boolean; + beforeCloseEditor(editor: EditorInput): void; + closeEditor(editor: EditorInput): void; + closeEditors(editors: EditorInput[]): void; + moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void; + pinEditor(editor: EditorInput): void; + stickEditor(editor: EditorInput): void; + unstickEditor(editor: EditorInput): void; + setActive(isActive: boolean): void; + updateEditorLabel(editor: EditorInput): void; + updateEditorDirty(editor: EditorInput): void; + layout(dimensions: IEditorTitleControlDimensions): Dimension; + getHeight(): number; +} + +export abstract class EditorTabsControl extends Themable implements IEditorTabsControl { protected readonly editorTransfer = LocalSelectionTransfer.getInstance(); protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -103,7 +122,8 @@ export abstract class EditorTabsControl extends Themable { constructor( private parent: HTMLElement, protected accessor: IEditorGroupsAccessor, - protected group: IEditorGroupView, + protected groupViewer: IEditorGroupView, + protected tabsModel: IReadonlyEditorGroupModel, @IContextMenuService protected readonly contextMenuService: IContextMenuService, @IInstantiationService protected instantiationService: IInstantiationService, @IContextKeyService protected readonly contextKeyService: IContextKeyService, @@ -139,7 +159,7 @@ export abstract class EditorTabsControl extends Themable { } protected createEditorActionsToolBar(container: HTMLElement): void { - const context: IEditorCommandsContext = { groupId: this.group.id }; + const context: IEditorCommandsContext = { groupId: this.groupViewer.id }; // Toolbar Widget @@ -171,7 +191,7 @@ export abstract class EditorTabsControl extends Themable { } private actionViewItemProvider(action: IAction): IActionViewItem | undefined { - const activeEditorPane = this.group.activeEditorPane; + const activeEditorPane = this.groupViewer.activeEditorPane; // Check Active Editor if (activeEditorPane instanceof EditorPane) { @@ -204,24 +224,24 @@ export abstract class EditorTabsControl extends Themable { // Update contexts this.contextKeyService.bufferChangeEvents(() => { - const activeEditor = this.group.activeEditor; + const activeEditor = this.groupViewer.activeEditor; this.resourceContext.set(EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY } ?? null)); - this.editorPinnedContext.set(activeEditor ? this.group.isPinned(activeEditor) : false); - this.editorIsFirstContext.set(activeEditor ? this.group.isFirst(activeEditor) : false); - this.editorIsLastContext.set(activeEditor ? this.group.isLast(activeEditor) : false); - this.editorStickyContext.set(activeEditor ? this.group.isSticky(activeEditor) : false); + this.editorPinnedContext.set(activeEditor ? this.groupViewer.isPinned(activeEditor) : false); + this.editorIsFirstContext.set(activeEditor ? this.groupViewer.isFirst(activeEditor) : false); + this.editorIsLastContext.set(activeEditor ? this.groupViewer.isLast(activeEditor) : false); + this.editorStickyContext.set(activeEditor ? this.groupViewer.isSticky(activeEditor) : false); applyAvailableEditorIds(this.editorAvailableEditorIds, activeEditor, this.editorResolverService); this.editorCanSplitInGroupContext.set(activeEditor ? activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup) : false); this.sideBySideEditorContext.set(activeEditor?.typeId === SideBySideEditorInput.ID); - this.groupLockedContext.set(this.group.isLocked); + this.groupLockedContext.set(this.groupViewer.isLocked); }); // Editor actions require the editor control to be there, so we retrieve it via service - const activeEditorPane = this.group.activeEditorPane; + const activeEditorPane = this.groupViewer.activeEditorPane; if (activeEditorPane instanceof EditorPane) { const scopedContextKeyService = this.getEditorPaneAwareContextKeyService(); const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService, { emitEventsForSubmenuChanges: true, eventDebounceDelay: 0 }); @@ -245,7 +265,7 @@ export abstract class EditorTabsControl extends Themable { } private getEditorPaneAwareContextKeyService(): IContextKeyService { - return this.group.activeEditorPane?.scopedContextKeyService ?? this.contextKeyService; + return this.groupViewer.activeEditorPane?.scopedContextKeyService ?? this.contextKeyService; } protected clearEditorActionsToolbar(): void { @@ -261,7 +281,7 @@ export abstract class EditorTabsControl extends Themable { } // Set editor group as transfer - this.groupTransfer.setData([new DraggedEditorGroupIdentifier(this.group.id)], DraggedEditorGroupIdentifier.prototype); + this.groupTransfer.setData([new DraggedEditorGroupIdentifier(this.groupViewer.id)], DraggedEditorGroupIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; } @@ -269,26 +289,26 @@ export abstract class EditorTabsControl extends Themable { // Drag all tabs of the group if tabs are enabled let hasDataTransfer = false; if (this.accessor.partOptions.showTabs) { - hasDataTransfer = this.doFillResourceDataTransfers(this.group.getEditors(EditorsOrder.SEQUENTIAL), e); + hasDataTransfer = this.doFillResourceDataTransfers(this.groupViewer.getEditors(EditorsOrder.SEQUENTIAL), e); } // Otherwise only drag the active editor else { - if (this.group.activeEditor) { - hasDataTransfer = this.doFillResourceDataTransfers([this.group.activeEditor], e); + if (this.groupViewer.activeEditor) { + hasDataTransfer = this.doFillResourceDataTransfers([this.groupViewer.activeEditor], e); } } // Firefox: requires to set a text data transfer to get going if (!hasDataTransfer && isFirefox) { - e.dataTransfer?.setData(DataTransfers.TEXT, String(this.group.label)); + e.dataTransfer?.setData(DataTransfers.TEXT, String(this.groupViewer.label)); } // Drag Image - if (this.group.activeEditor) { - let label = this.group.activeEditor.getName(); - if (this.accessor.partOptions.showTabs && this.group.count > 1) { - label = localize('draggedEditorGroup', "{0} (+{1})", label, this.group.count - 1); + if (this.groupViewer.activeEditor) { + let label = this.groupViewer.activeEditor.getName(); + if (this.accessor.partOptions.showTabs && this.groupViewer.count > 1) { + label = localize('draggedEditorGroup', "{0} (+{1})", label, this.groupViewer.count - 1); } applyDragImage(e, label, 'monaco-editor-group-drag-image', this.getColor(listActiveSelectionBackground), this.getColor(listActiveSelectionForeground)); @@ -303,7 +323,7 @@ export abstract class EditorTabsControl extends Themable { protected doFillResourceDataTransfers(editors: readonly EditorInput[], e: DragEvent): boolean { if (editors.length) { - this.instantiationService.invokeFunction(fillEditorsDragData, editors.map(editor => ({ editor, groupId: this.group.id })), e); + this.instantiationService.invokeFunction(fillEditorsDragData, editors.map(editor => ({ editor, groupId: this.groupViewer.id })), e); return true; } @@ -311,21 +331,21 @@ export abstract class EditorTabsControl extends Themable { return false; } - protected onContextMenu(editor: EditorInput, e: Event, node: HTMLElement): void { + protected onTabContextMenu(editor: EditorInput, e: Event, node: HTMLElement): void { // Update contexts based on editor picked and remember previous to restore const currentResourceContext = this.resourceContext.get(); this.resourceContext.set(EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY } ?? null)); const currentPinnedContext = !!this.editorPinnedContext.get(); - this.editorPinnedContext.set(this.group.isPinned(editor)); + this.editorPinnedContext.set(this.tabsModel.isPinned(editor)); const currentEditorIsFirstContext = !!this.editorIsFirstContext.get(); - this.editorIsFirstContext.set(this.group.isFirst(editor)); + this.editorIsFirstContext.set(this.tabsModel.isFirst(editor)); const currentEditorIsLastContext = !!this.editorIsLastContext.get(); - this.editorIsLastContext.set(this.group.isLast(editor)); + this.editorIsLastContext.set(this.tabsModel.isLast(editor)); const currentStickyContext = !!this.editorStickyContext.get(); - this.editorStickyContext.set(this.group.isSticky(editor)); + this.editorStickyContext.set(this.tabsModel.isSticky(editor)); const currentGroupLockedContext = !!this.groupLockedContext.get(); - this.groupLockedContext.set(this.group.isLocked); + this.groupLockedContext.set(this.tabsModel.isLocked); const currentEditorCanSplitContext = !!this.editorCanSplitInGroupContext.get(); this.editorCanSplitInGroupContext.set(editor.hasCapability(EditorInputCapabilities.CanSplitInGroup)); const currentSideBySideEditorContext = !!this.sideBySideEditorContext.get(); @@ -345,7 +365,7 @@ export abstract class EditorTabsControl extends Themable { menuId: MenuId.EditorTitleContext, menuActionOptions: { shouldForwardArgs: true, arg: this.resourceContext.get() }, contextKeyService: this.contextKeyService, - getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) }), + getActionsContext: () => ({ groupId: this.groupViewer.id, editorIndex: this.groupViewer.getIndexOfEditor(editor) }), getKeyBinding: action => this.getKeybinding(action), onHide: () => { diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index ecdc4f67a57..dc3b68698ad 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -9,12 +9,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { BreadcrumbsControl, BreadcrumbsControlFactory } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IEditorGroupsAccessor, IEditorGroupTitleHeight, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { EditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; +import { IEditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { MultiEditorTabsControl } from 'vs/workbench/browser/parts/editor/multiEditorTabsControl'; import { SingleEditorTabsControl } from 'vs/workbench/browser/parts/editor/singleEditorTabsControl'; import { IEditorPartOptions } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { MultiRowEditorControl } from 'vs/workbench/browser/parts/editor/multiRowEditorTabsControl'; +import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; export interface IEditorTitleControlDimensions { @@ -32,7 +34,7 @@ export interface IEditorTitleControlDimensions { export class EditorTitleControl extends Themable { - private editorTabsControl: EditorTabsControl; + private editorTabsControl: IEditorTabsControl; private editorTabsControlDisposable = this._register(new DisposableStore()); private breadcrumbsControlFactory: BreadcrumbsControlFactory | undefined; @@ -42,7 +44,8 @@ export class EditorTitleControl extends Themable { constructor( private parent: HTMLElement, private accessor: IEditorGroupsAccessor, - private group: IEditorGroupView, + private groupViewer: IEditorGroupView, + private model: IReadonlyEditorGroupModel, @IInstantiationService private instantiationService: IInstantiationService, @IThemeService themeService: IThemeService ) { @@ -52,12 +55,16 @@ export class EditorTitleControl extends Themable { this.breadcrumbsControlFactory = this.createBreadcrumbsControl(); } - private createEditorTabsControl(): EditorTabsControl { - let control: EditorTabsControl; + private createEditorTabsControl(): IEditorTabsControl { + let control: IEditorTabsControl; if (this.accessor.partOptions.showTabs) { - control = this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.group); + if (this.accessor.partOptions.pinnedTabsOnSeparateRow) { + control = this.instantiationService.createInstance(MultiRowEditorControl, this.parent, this.accessor, this.groupViewer, this.model); + } else { + control = this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.groupViewer, this.model); + } } else { - control = this.instantiationService.createInstance(SingleEditorTabsControl, this.parent, this.accessor, this.group); + control = this.instantiationService.createInstance(SingleEditorTabsControl, this.parent, this.accessor, this.groupViewer, this.model); } return this.editorTabsControlDisposable.add(control); @@ -73,7 +80,7 @@ export class EditorTitleControl extends Themable { breadcrumbsContainer.classList.add('breadcrumbs-below-tabs'); this.parent.appendChild(breadcrumbsContainer); - const breadcrumbsControlFactory = this.breadcrumbsControlDisposables.add(this.instantiationService.createInstance(BreadcrumbsControlFactory, breadcrumbsContainer, this.group, { + const breadcrumbsControlFactory = this.breadcrumbsControlDisposables.add(this.instantiationService.createInstance(BreadcrumbsControlFactory, breadcrumbsContainer, this.groupViewer, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, @@ -85,7 +92,7 @@ export class EditorTitleControl extends Themable { } private handleBreadcrumbsEnablementChange(): void { - this.group.relayout(); // relayout when breadcrumbs are enable/disabled + this.groupViewer.relayout(); // relayout when breadcrumbs are enable/disabled } openEditor(editor: EditorInput): void { @@ -125,13 +132,13 @@ export class EditorTitleControl extends Themable { } private handleClosedEditors(): void { - if (!this.group.activeEditor) { + if (!this.groupViewer.activeEditor) { this.breadcrumbsControl?.update(); } } - moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number): void { - return this.editorTabsControl.moveEditor(editor, fromIndex, targetIndex); + moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void { + return this.editorTabsControl.moveEditor(editor, fromIndex, targetIndex, stickyStateChange); } pinEditor(editor: EditorInput): void { @@ -159,10 +166,11 @@ export class EditorTitleControl extends Themable { } updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { - // Update editor tabs control if options changed - if (oldOptions.showTabs !== newOptions.showTabs) { - + if ( + oldOptions.showTabs !== newOptions.showTabs || + (newOptions.showTabs && oldOptions.pinnedTabsOnSeparateRow !== newOptions.pinnedTabsOnSeparateRow) + ) { // Clear old this.editorTabsControlDisposable.clear(); this.breadcrumbsControlDisposables.clear(); @@ -174,7 +182,9 @@ export class EditorTitleControl extends Themable { } // Forward into editor tabs control - this.editorTabsControl.updateOptions(oldOptions, newOptions); + else { + this.editorTabsControl.updateOptions(oldOptions, newOptions); + } } layout(dimensions: IEditorTitleControlDimensions): Dimension { diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index d51863edf23..e390b16856a 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -39,13 +39,17 @@ ########################################################################################## */ -/* Title Container */ +/* Tabs and Actions Container */ .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { display: flex; position: relative; /* position tabs border bottom or editor actions (when tabs wrap) relative to this container */ } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.empty { + display: none; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.tabs-border-bottom::after { content: ''; position: absolute; @@ -444,3 +448,9 @@ bottom: 0; right: 0; } + +.monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars > .tabs-and-actions-container:first-child .editor-actions { + + /* When multiple tab bars are visible, only show editor actions for the last tab bar */ + display: none; +} diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index e3e4de2bc0e..6073b594f0d 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/multieditortabscontrol'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { shorten } from 'vs/base/common/labels'; -import { EditorResourceAccessor, GroupIdentifier, Verbosity, IEditorPartOptions, SideBySideEditor, DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IUntypedEditorInput, preventEditorClose, EditorCloseMethod } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, GroupIdentifier, Verbosity, IEditorPartOptions, SideBySideEditor, DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IUntypedEditorInput, preventEditorClose, EditorCloseMethod, EditorsOrder } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -34,7 +34,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, EditorServiceImpl, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -46,13 +46,15 @@ import { coalesce, insert } from 'vs/base/common/arrays'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { isSafari } from 'vs/base/browser/browser'; import { equals } from 'vs/base/common/objects'; -import { EditorActivation } from 'vs/platform/editor/common/editor'; +import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; import { UNLOCK_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor/editorTitleControl'; +import { StickyEditorGroupModel, UnstickyEditorGroupModel } from 'vs/workbench/common/editor/filteredEditorGroupModel'; +import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; interface IEditorInputLabel { readonly editor: EditorInput; @@ -133,7 +135,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { constructor( parent: HTMLElement, accessor: IEditorGroupsAccessor, - group: IEditorGroupView, + groupViewer: IEditorGroupView, + tabsModel: IReadonlyEditorGroupModel, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @@ -148,7 +151,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { @ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService, @IEditorResolverService editorResolverService: IEditorResolverService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, editorResolverService); + super(parent, accessor, groupViewer, tabsModel, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, editorResolverService); // Resolve the correct path library for the OS we are on // If we are connected to remote, this accounts for the @@ -193,6 +196,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Editor Actions Toolbar this.createEditorActionsToolBar(this.editorToolbarContainer); + + // Set tabs control visibility + this.updateTabsControlVisibility(); } private createTabsScrollbar(scrollable: HTMLElement): ScrollableElement { @@ -249,7 +255,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private updateTabsFixedWidth(fixed: boolean): void { - this.forEachTab((editor, index, tabContainer) => { + this.forEachTab((editor, tabIndex, tabContainer) => { if (fixed) { const { width } = tabContainer.getBoundingClientRect(); tabContainer.style.setProperty('--tab-sizing-current-width', `${width}px`); @@ -304,10 +310,10 @@ export class MultiEditorTabsControl extends EditorTabsControl { resource: undefined, options: { pinned: true, - index: this.group.count, // always at the end + index: this.groupViewer.count, // always at the end override: DEFAULT_EDITOR_ASSOCIATION.id } - }, this.group.id); + }, this.groupViewer.id); })); } @@ -348,7 +354,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { const localDraggedEditor = data[0].identifier; - if (this.group.id === localDraggedEditor.groupId && this.group.getIndexOfEditor(localDraggedEditor.editor) === this.group.count - 1) { + if (this.groupViewer.id === localDraggedEditor.groupId && this.tabsModel.isLast(localDraggedEditor.editor)) { if (e.dataTransfer) { e.dataTransfer.dropEffect = 'none'; } @@ -384,15 +390,15 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabsContainer.classList.remove('scroll'); if (e.target === tabsContainer) { - this.onDrop(e, this.group.count, tabsContainer); + this.onDrop(e, this.tabsModel.count, tabsContainer); } } })); // Mouse-wheel support to switch to tabs optionally this._register(addDisposableListener(tabsContainer, EventType.MOUSE_WHEEL, (e: WheelEvent) => { - const activeEditor = this.group.activeEditor; - if (!activeEditor || this.group.count < 2) { + const activeEditor = this.groupViewer.activeEditor; + if (!activeEditor || this.groupViewer.count < 2) { return; // need at least 2 open editors } @@ -427,13 +433,13 @@ export class MultiEditorTabsControl extends EditorTabsControl { return; } - const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + tabSwitchDirection); + const nextEditor = this.groupViewer.getEditorByIndex(this.groupViewer.getIndexOfEditor(activeEditor) + tabSwitchDirection); if (!nextEditor) { return; } // Open it - this.group.openEditor(nextEditor); + this.groupViewer.openEditor(nextEditor); // Disable normal scrolling, opening the editor will already reveal it properly EventHelper.stop(e, true); @@ -455,9 +461,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { menuId: MenuId.EditorTabsBarContext, contextKeyService: this.contextKeyService, menuActionOptions: { shouldForwardArgs: true }, - getActionsContext: () => ({ groupId: this.group.id }), + getActionsContext: () => ({ groupId: this.groupViewer.id }), getKeyBinding: action => this.getKeybinding(action), - onHide: () => this.group.focus() + onHide: () => this.groupViewer.focus() }); }; @@ -490,9 +496,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { private handleOpenedEditors(): boolean { + // Set tabs control visibility + this.updateTabsControlVisibility(); + // Create tabs as needed const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); - for (let i = tabsContainer.children.length; i < this.group.count; i++) { + for (let i = tabsContainer.children.length; i < this.tabsModel.count; i++) { tabsContainer.appendChild(this.createTab(i, tabsContainer, tabsScrollbar)); } @@ -526,9 +535,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { private didActiveEditorChange(): boolean { if ( - !this.activeTabLabel?.editor && this.group.activeEditor || // active editor changed from null => editor - this.activeTabLabel?.editor && !this.group.activeEditor || // active editor changed from editor => null - (!this.activeTabLabel?.editor || !this.group.isActive(this.activeTabLabel.editor)) // active editor changed from editorA => editorB + !this.activeTabLabel?.editor && this.tabsModel.activeEditor || // active editor changed from null => editor + this.activeTabLabel?.editor && !this.tabsModel.activeEditor || // active editor changed from editor => null + (!this.activeTabLabel?.editor || !this.tabsModel.isActive(this.activeTabLabel.editor)) // active editor changed from editorA => editorB ) { return true; } @@ -560,7 +569,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // the mouse and allows for rapid closing of tabs. if (this.isMouseOverTabs && this.accessor.partOptions.tabSizing === 'fixed') { - const closingLastTab = this.group.isLast(editor); + const closingLastTab = this.tabsModel.isLast(editor); this.updateTabsFixedWidth(!closingLastTab); } } @@ -576,11 +585,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { private handleClosedEditors(): void { // There are tabs to show - if (this.group.activeEditor) { + if (this.tabsModel.count) { // Remove tabs that got closed const tabsContainer = assertIsDefined(this.tabsContainer); - while (tabsContainer.children.length > this.group.count) { + while (tabsContainer.children.length > this.tabsModel.count) { // Remove one tab from container (must be the last to keep indexes in order!) tabsContainer.lastChild?.remove(); @@ -609,22 +618,23 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.tabActionBars = []; this.clearEditorActionsToolbar(); + this.updateTabsControlVisibility(); } } - moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number): void { + moveEditor(editor: EditorInput, fromTabIndex: number, targeTabIndex: number): void { // Move the editor label - const editorLabel = this.tabLabels[fromIndex]; - this.tabLabels.splice(fromIndex, 1); - this.tabLabels.splice(targetIndex, 0, editorLabel); + const editorLabel = this.tabLabels[fromTabIndex]; + this.tabLabels.splice(fromTabIndex, 1); + this.tabLabels.splice(targeTabIndex, 0, editorLabel); // Redraw tabs in the range of the move - this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { - this.redrawTab(editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar); + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { + this.redrawTab(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar); }, - Math.min(fromIndex, targetIndex), // from: smallest of fromIndex/targetIndex - Math.max(fromIndex, targetIndex) // to: largest of fromIndex/targetIndex + Math.min(fromTabIndex, targeTabIndex), // from: smallest of fromTabIndex/targeTabIndex + Math.max(fromTabIndex, targeTabIndex) // to: largest of fromTabIndex/targeTabIndex ); // Moving an editor requires a layout to keep the active editor visible @@ -632,7 +642,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } pinEditor(editor: EditorInput): void { - this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel) => this.redrawTabLabel(editor, index, tabContainer, tabLabelWidget, tabLabel)); + this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel) => this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel)); } stickEditor(editor: EditorInput): void { @@ -646,12 +656,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { private doHandleStickyEditorChange(editor: EditorInput): void { // Update tab - this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTab(editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar)); + this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTab(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar)); // Sticky change has an impact on each tab's border because // it potentially moves the border to the last pinned tab - this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel) => { - this.redrawTabBorders(index, tabContainer); + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel) => { + this.redrawTabBorders(tabIndex, tabContainer); }); // A change to the sticky state requires a layout to keep the active editor visible @@ -661,7 +671,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { setActive(isGroupActive: boolean): void { // Activity has an impact on each tab's active indication - this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { this.redrawTabActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); }); @@ -688,8 +698,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.computeTabLabels(); // As such we need to redraw each label - this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel) => { - this.redrawTabLabel(editor, index, tabContainer, tabLabelWidget, tabLabel); + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel) => { + this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel); }); // A change to a label requires a layout to keep the active editor visible @@ -697,7 +707,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } updateEditorDirty(editor: EditorInput): void { - this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabActionBar)); + this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.groupViewer, editor, tabContainer, tabActionBar)); } override updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { @@ -742,36 +752,36 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.redraw(); } - private forEachTab(fn: (editor: EditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar) => void, fromIndex?: number, toIndex?: number): void { - this.group.editors.forEach((editor, index) => { - if (typeof fromIndex === 'number' && fromIndex > index) { + private forEachTab(fn: (editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar) => void, fromTabIndex?: number, toTabIndex?: number): void { + this.tabsModel.getEditors(EditorsOrder.SEQUENTIAL).forEach((editor: EditorInput, tabIndex: number) => { + if (typeof fromTabIndex === 'number' && fromTabIndex > tabIndex) { return; // do nothing if we are not yet at `fromIndex` } - if (typeof toIndex === 'number' && toIndex < index) { + if (typeof toTabIndex === 'number' && toTabIndex < tabIndex) { return; // do nothing if we are beyond `toIndex` } - this.doWithTab(index, editor, fn); + this.doWithTab(tabIndex, editor, fn); }); } - private withTab(editor: EditorInput, fn: (editor: EditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar) => void): void { - this.doWithTab(this.group.getIndexOfEditor(editor), editor, fn); + private withTab(editor: EditorInput, fn: (editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar) => void): void { + this.doWithTab(this.tabsModel.indexOf(editor), editor, fn); } - private doWithTab(index: number, editor: EditorInput, fn: (editor: EditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar) => void): void { + private doWithTab(tabIndex: number, editor: EditorInput, fn: (editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar) => void): void { const tabsContainer = assertIsDefined(this.tabsContainer); - const tabContainer = tabsContainer.children[index] as HTMLElement; - const tabResourceLabel = this.tabResourceLabels.get(index); - const tabLabel = this.tabLabels[index]; - const tabActionBar = this.tabActionBars[index]; + const tabContainer = tabsContainer.children[tabIndex] as HTMLElement; + const tabResourceLabel = this.tabResourceLabels.get(tabIndex); + const tabLabel = this.tabLabels[tabIndex]; + const tabActionBar = this.tabActionBars[tabIndex]; if (tabContainer && tabResourceLabel && tabLabel) { - fn(editor, index, tabContainer, tabResourceLabel, tabLabel, tabActionBar); + fn(editor, tabIndex, tabContainer, tabResourceLabel, tabLabel, tabActionBar); } } - private createTab(index: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): HTMLElement { + private createTab(tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): HTMLElement { // Tab Container const tabContainer = document.createElement('div'); @@ -795,7 +805,15 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabActionsContainer.classList.add('tab-actions'); tabContainer.appendChild(tabActionsContainer); - const tabActionRunner = new EditorCommandsContextActionRunner({ groupId: this.group.id, editorIndex: index }); + const that = this; + const tabActionRunner = new EditorCommandsContextActionRunner({ + groupId: this.groupViewer.id, + get editorIndex() { + const editor = assertIsDefined(that.tabsModel.getEditorByIndex(tabIndex)); + + return that.groupViewer.getIndexOfEditor(editor); + }, + }); const tabActionBar = new ActionBar(tabActionsContainer, { ariaLabel: localize('ariaLabelTabActions', "Tab actions"), actionRunner: tabActionRunner }); const tabActionListener = tabActionBar.onWillRun(e => { @@ -812,14 +830,14 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabContainer.appendChild(tabBorderBottomContainer); // Eventing - const eventsDisposable = this.registerTabListeners(tabContainer, index, tabsContainer, tabsScrollbar); + const eventsDisposable = this.registerTabListeners(tabContainer, tabIndex, tabsContainer, tabsScrollbar); this.tabDisposables.push(combinedDisposable(eventsDisposable, tabActionBarDisposable, tabActionRunner, editorLabel)); return tabContainer; } - private registerTabListeners(tab: HTMLElement, index: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable { + private registerTabListeners(tab: HTMLElement, tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable { const disposables = new DisposableStore(); const handleClickOrTouch = (e: MouseEvent | GestureEvent, preserveFocus: boolean): void => { @@ -838,10 +856,10 @@ export class MultiEditorTabsControl extends EditorTabsControl { } // Open tabs editor - const editor = this.group.getEditorByIndex(index); + const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { // Even if focus is preserved make sure to activate the group. - this.group.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }); + this.groupViewer.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }); } return undefined; @@ -850,9 +868,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { const showContextMenu = (e: Event) => { EventHelper.stop(e); - const editor = this.group.getEditorByIndex(index); + const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - this.onContextMenu(editor, e, tab); + this.onTabContextMenu(editor, e, tab); } }; @@ -877,13 +895,15 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (e.button === 1 /* Middle Button*/) { EventHelper.stop(e, true /* for https://github.com/microsoft/vscode/issues/56715 */); - const editor = this.group.getEditorByIndex(index); - if (editor && preventEditorClose(this.group, editor, EditorCloseMethod.MOUSE, this.accessor.partOptions)) { - return; - } + const editor = this.tabsModel.getEditorByIndex(tabIndex); + if (editor) { + if (preventEditorClose(this.tabsModel, editor, EditorCloseMethod.MOUSE, this.accessor.partOptions)) { + return; + } - this.blockRevealActiveTabOnce(); - this.closeEditorAction.run({ groupId: this.group.id, editorIndex: index }); + this.blockRevealActiveTabOnce(); + this.closeEditorAction.run({ groupId: this.groupViewer.id, editorIndex: this.groupViewer.getIndexOfEditor(editor) }); + } } })); @@ -908,30 +928,30 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Run action on Enter/Space if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { handled = true; - const editor = this.group.getEditorByIndex(index); + const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - this.group.openEditor(editor); + this.groupViewer.openEditor(editor); } } // Navigate in editors else if ([KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.Home, KeyCode.End].some(kb => event.equals(kb))) { - let targetIndex: number; + let tabTargetIndex: number; if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.UpArrow)) { - targetIndex = index - 1; + tabTargetIndex = tabIndex - 1; } else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.DownArrow)) { - targetIndex = index + 1; + tabTargetIndex = tabIndex + 1; } else if (event.equals(KeyCode.Home)) { - targetIndex = 0; + tabTargetIndex = 0; } else { - targetIndex = this.group.count - 1; + tabTargetIndex = this.tabsModel.count - 1; } - const target = this.group.getEditorByIndex(targetIndex); + const target = this.tabsModel.getEditorByIndex(tabTargetIndex); if (target) { handled = true; - this.group.openEditor(target, { preserveFocus: true }); - (tabsContainer.childNodes[targetIndex]).focus(); + this.groupViewer.openEditor(target, { preserveFocus: true }); + (tabsContainer.childNodes[tabTargetIndex]).focus(); } } @@ -954,13 +974,13 @@ export class MultiEditorTabsControl extends EditorTabsControl { return; // ignore single taps } - const editor = this.group.getEditorByIndex(index); - if (editor && this.group.isPinned(editor)) { + const editor = this.tabsModel.getEditorByIndex(tabIndex); + if (editor && this.tabsModel.isPinned(editor)) { if (this.accessor.partOptions.doubleClickTabToToggleEditorGroupSizes) { - this.accessor.arrangeGroups(GroupsArrangement.TOGGLE, this.group); + this.accessor.arrangeGroups(GroupsArrangement.TOGGLE, this.groupViewer); } } else { - this.group.pinEditor(editor); + this.groupViewer.pinEditor(editor); } })); } @@ -969,20 +989,20 @@ export class MultiEditorTabsControl extends EditorTabsControl { disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, e => { EventHelper.stop(e, true); - const editor = this.group.getEditorByIndex(index); + const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - this.onContextMenu(editor, e, tab); + this.onTabContextMenu(editor, e, tab); } }, true /* use capture to fix https://github.com/microsoft/vscode/issues/19145 */)); // Drag support disposables.add(addDisposableListener(tab, EventType.DRAG_START, e => { - const editor = this.group.getEditorByIndex(index); + const editor = this.tabsModel.getEditorByIndex(tabIndex); if (!editor) { return; } - this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, groupId: this.group.id })], DraggedEditorIdentifier.prototype); + this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, groupId: this.groupViewer.id })], DraggedEditorIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; @@ -1020,7 +1040,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { const localDraggedEditor = data[0].identifier; - if (localDraggedEditor.editor === this.group.getEditorByIndex(index) && localDraggedEditor.groupId === this.group.id) { + if (localDraggedEditor.editor === this.tabsModel.getEditorByIndex(tabIndex) && localDraggedEditor.groupId === this.groupViewer.id) { if (e.dataTransfer) { e.dataTransfer.dropEffect = 'none'; } @@ -1038,35 +1058,35 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } - this.updateDropFeedback(tab, true, index); + this.updateDropFeedback(tab, true, tabIndex); }, onDragOver: (_, dragDuration) => { if (dragDuration >= MultiEditorTabsControl.DRAG_OVER_OPEN_TAB_THRESHOLD) { - const draggedOverTab = this.group.getEditorByIndex(index); - if (draggedOverTab && this.group.activeEditor !== draggedOverTab) { - this.group.openEditor(draggedOverTab, { preserveFocus: true }); + const draggedOverTab = this.tabsModel.getEditorByIndex(tabIndex); + if (draggedOverTab && this.tabsModel.activeEditor !== draggedOverTab) { + this.groupViewer.openEditor(draggedOverTab, { preserveFocus: true }); } } }, onDragLeave: () => { tab.classList.remove('dragged-over'); - this.updateDropFeedback(tab, false, index); + this.updateDropFeedback(tab, false, tabIndex); }, onDragEnd: () => { tab.classList.remove('dragged-over'); - this.updateDropFeedback(tab, false, index); + this.updateDropFeedback(tab, false, tabIndex); this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); }, onDrop: e => { tab.classList.remove('dragged-over'); - this.updateDropFeedback(tab, false, index); + this.updateDropFeedback(tab, false, tabIndex); - this.onDrop(e, index, tabsContainer); + this.onDrop(e, tabIndex, tabsContainer); } })); @@ -1078,7 +1098,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { const group = data[0]; - if (group.identifier === this.group.id) { + if (group.identifier === this.groupViewer.id) { return false; // groups cannot be dropped on group it originates from } } @@ -1097,10 +1117,10 @@ export class MultiEditorTabsControl extends EditorTabsControl { return false; } - private updateDropFeedback(element: HTMLElement, isDND: boolean, index?: number): void { - const isTab = (typeof index === 'number'); - const editor = typeof index === 'number' ? this.group.getEditorByIndex(index) : undefined; - const isActiveTab = isTab && !!editor && this.group.isActive(editor); + private updateDropFeedback(element: HTMLElement, isDND: boolean, tabIndex?: number): void { + const isTab = (typeof tabIndex === 'number'); + const editor = typeof tabIndex === 'number' ? this.tabsModel.getEditorByIndex(tabIndex) : undefined; + const isActiveTab = isTab && !!editor && this.tabsModel.isActive(editor); // Background const noDNDBackgroundColor = isTab ? this.getColor(isActiveTab ? TAB_ACTIVE_BACKGROUND : TAB_INACTIVE_BACKGROUND) : ''; @@ -1127,22 +1147,21 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Build labels and descriptions for each editor const labels: IEditorInputLabel[] = []; - let activeEditorIndex = -1; - for (let i = 0; i < this.group.editors.length; i++) { - const editor = this.group.editors[i]; + let activeEditorTabIndex = -1; + this.tabsModel.getEditors(EditorsOrder.SEQUENTIAL).forEach((editor: EditorInput, tabIndex: number) => { labels.push({ editor, name: editor.getName(), description: editor.getDescription(verbosity), forceDescription: editor.hasCapability(EditorInputCapabilities.ForceDescription), title: editor.getTitle(Verbosity.LONG), - ariaLabel: computeEditorAriaLabel(editor, i, this.group, this.editorGroupService.count) + ariaLabel: computeEditorAriaLabel(editor, tabIndex, this.groupViewer, this.editorGroupService.count) }); - if (editor === this.group.activeEditor) { - activeEditorIndex = i; + if (editor === this.tabsModel.activeEditor) { + activeEditorTabIndex = tabIndex; } - } + }); // Shorten labels as needed if (shortenDuplicates) { @@ -1151,7 +1170,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Remember for fast lookup this.tabLabels = labels; - this.activeTabLabel = labels[activeEditorIndex]; + this.activeTabLabel = labels[activeEditorTabIndex]; } private shortenTabLabels(labels: IEditorInputLabel[]): void { @@ -1220,9 +1239,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Shorten descriptions const shortenedDescriptions = shorten(descriptions, this.path.sep); - descriptions.forEach((description, index) => { + descriptions.forEach((description, tabIndex) => { for (const label of mapDescriptionToDuplicates.get(description) || []) { - label.description = shortenedDescriptions[index]; + label.description = shortenedDescriptions[tabIndex]; } }); } @@ -1260,8 +1279,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { } // For each tab - this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { - this.redrawTab(editor, index, tabContainer, tabLabelWidget, tabLabel, tabActionBar); + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { + this.redrawTab(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar); }); // Update Editor Actions Toolbar @@ -1271,12 +1290,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.layout(this.dimensions, options); } - private redrawTab(editor: EditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void { - const isTabSticky = this.group.isSticky(index); + private redrawTab(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void { + const isTabSticky = this.tabsModel.isSticky(tabIndex); const options = this.accessor.partOptions; // Label - this.redrawTabLabel(editor, index, tabContainer, tabLabelWidget, tabLabel); + this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel); // Action const tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction; @@ -1319,19 +1338,19 @@ export class MultiEditorTabsControl extends EditorTabsControl { break; } - tabContainer.style.left = `${index * stickyTabWidth}px`; + tabContainer.style.left = `${tabIndex * stickyTabWidth}px`; } else { tabContainer.style.left = 'auto'; } // Borders / outline - this.redrawTabBorders(index, tabContainer); + this.redrawTabBorders(tabIndex, tabContainer); // Active / dirty state - this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabActionBar); + this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.groupViewer, editor, tabContainer, tabActionBar); } - private redrawTabLabel(editor: EditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { + private redrawTabLabel(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { const options = this.accessor.partOptions; // Unless tabs are sticky compact, show the full label and description @@ -1341,7 +1360,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { let forceLabel = false; let fileDecorationBadges = Boolean(options.decorations?.badges); let description: string; - if (options.pinnedTabSizing === 'compact' && this.group.isSticky(index)) { + if (options.pinnedTabSizing === 'compact' && this.tabsModel.isSticky(tabIndex)) { const isShowingIcons = options.showIcons && options.hasIcons; name = isShowingIcons ? '' : tabLabel.name?.charAt(0).toUpperCase(); description = ''; @@ -1368,7 +1387,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { { title, extraClasses: coalesce(['tab-label', fileDecorationBadges ? 'tab-label-has-badge' : undefined].concat(editor.getLabelExtraClasses())), - italic: !this.group.isPinned(editor), + italic: !this.tabsModel.isPinned(editor), forceLabel, fileDecorations: { colors: Boolean(options.decorations?.colors), @@ -1387,7 +1406,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private redrawTabActiveAndDirty(isGroupActive: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { - const isTabActive = this.group.isActive(editor); + const isTabActive = this.tabsModel.isActive(editor); const hasModifiedBorderTop = this.doRedrawTabDirty(isGroupActive, isTabActive, editor, tabContainer); this.doRedrawTabActive(isGroupActive, !hasModifiedBorderTop, editor, tabContainer, tabActionBar); @@ -1396,7 +1415,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { // Tab is active - if (this.group.isActive(editor)) { + if (this.tabsModel.isActive(editor)) { // Container tabContainer.classList.add('active'); @@ -1488,9 +1507,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { return hasModifiedBorderColor; } - private redrawTabBorders(index: number, tabContainer: HTMLElement): void { - const isTabSticky = this.group.isSticky(index); - const isTabLastSticky = isTabSticky && this.group.stickyCount === index + 1; + private redrawTabBorders(tabIndex: number, tabContainer: HTMLElement): void { + const isTabSticky = this.tabsModel.isSticky(tabIndex); + const isTabLastSticky = isTabSticky && this.tabsModel.stickyCount === tabIndex + 1; // Borders / Outline const borderRightColor = ((isTabLastSticky ? this.getColor(TAB_LAST_PINNED_BORDER) : undefined) || this.getColor(TAB_BORDER) || this.getColor(contrastBorder)); @@ -1499,7 +1518,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } protected override prepareEditorActions(editorActions: IToolbarActions): IToolbarActions { - const isGroupActive = this.accessor.activeGroup === this.group; + const isGroupActive = this.accessor.activeGroup === this.groupViewer; // Active: allow all actions if (isGroupActive) { @@ -1531,9 +1550,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { private computeHeight(): number { let height: number; - // Wrap: we need to ask `offsetHeight` to get - // the real height of the title area with wrapping. - if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { + if (!this.visible) { + height = 0; + } else if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { + // Wrap: we need to ask `offsetHeight` to get + // the real height of the title area with wrapping. height = this.tabsAndActionsContainer.offsetHeight; } else { height = this.tabHeight; @@ -1547,25 +1568,27 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Remember dimensions that we get Object.assign(this.dimensions, dimensions); - // The layout of tabs can be an expensive operation because we access DOM properties - // that can result in the browser doing a full page layout to validate them. To buffer - // this a little bit we try at least to schedule this work on the next animation frame. - if (!this.layoutScheduler.value) { - const scheduledLayout = scheduleAtNextAnimationFrame(() => { - this.doLayout(this.dimensions, this.layoutScheduler.value?.options /* ensure to pick up latest options */); + if (this.visible) { + // The layout of tabs can be an expensive operation because we access DOM properties + // that can result in the browser doing a full page layout to validate them. To buffer + // this a little bit we try at least to schedule this work on the next animation frame. + if (!this.layoutScheduler.value) { + const scheduledLayout = scheduleAtNextAnimationFrame(() => { + this.doLayout(this.dimensions, this.layoutScheduler.value?.options /* ensure to pick up latest options */); - this.layoutScheduler.clear(); - }); + this.layoutScheduler.clear(); + }); - this.layoutScheduler.value = { options, dispose: () => scheduledLayout.dispose() }; - } + this.layoutScheduler.value = { options, dispose: () => scheduledLayout.dispose() }; + } - // Make sure to keep options updated - if (options?.forceRevealActiveTab) { - this.layoutScheduler.value.options = { - ...this.layoutScheduler.value.options, - forceRevealActiveTab: true - }; + // Make sure to keep options updated + if (options?.forceRevealActiveTab) { + this.layoutScheduler.value.options = { + ...this.layoutScheduler.value.options, + forceRevealActiveTab: true + }; + } } // First time layout: compute the dimensions and store it @@ -1578,13 +1601,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { private doLayout(dimensions: IEditorTitleControlDimensions, options?: IMultiEditorTabsControlLayoutOptions): void { - // Only layout if we have valid tab index and dimensions - const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (activeTabAndIndex && dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) { - - // Tabs - const [activeTab, activeIndex] = activeTabAndIndex; - this.doLayoutTabs(activeTab, activeIndex, dimensions, options); + // Layout tabs + if (dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) { + this.doLayoutTabs(dimensions, options); } // Remember the dimensions used in the control so that we can @@ -1598,11 +1617,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { // to signal this to the outside via a `relayout` call so that // e.g. the editor control can be adjusted accordingly. if (oldDimension && oldDimension.height !== newDimension.height) { - this.group.relayout(); + this.groupViewer.relayout(); } } - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: IEditorTitleControlDimensions, options?: IMultiEditorTabsControlLayoutOptions): void { + private doLayoutTabs(dimensions: IEditorTitleControlDimensions, options?: IMultiEditorTabsControlLayoutOptions): void { // Always first layout tabs with wrapping support even if wrapping // is disabled. The result indicates if tabs wrap and if not, we @@ -1611,7 +1630,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // wrapping is disabled (e.g. due to space constraints) const tabsWrapMultiLine = this.doLayoutTabsWrapping(dimensions); if (!tabsWrapMultiLine) { - this.doLayoutTabsNonWrapping(activeTab, activeIndex, options); + this.doLayoutTabsNonWrapping(options); } } @@ -1744,7 +1763,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { return tabsWrapMultiLine; } - private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number, options?: IMultiEditorTabsControlLayoutOptions): void { + private doLayoutTabsNonWrapping(options?: IMultiEditorTabsControlLayoutOptions): void { const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); // @@ -1771,7 +1790,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // - shrink: sticky-tabs * TAB_SIZES.shrink // - normal: 0 (sticky tabs inherit look and feel from non-sticky tabs) let stickyTabsWidth = 0; - if (this.group.stickyCount > 0) { + if (this.tabsModel.stickyCount > 0) { let stickyTabWidth = 0; switch (this.accessor.partOptions.pinnedTabSizing) { case 'compact': @@ -1782,18 +1801,21 @@ export class MultiEditorTabsControl extends EditorTabsControl { break; } - stickyTabsWidth = this.group.stickyCount * stickyTabWidth; + stickyTabsWidth = this.tabsModel.stickyCount * stickyTabWidth; } + const activeTabAndIndex = this.tabsModel.activeEditor ? this.getTabAndIndex(this.tabsModel.activeEditor) : undefined; + const [activeTab, activeTabIndex] = activeTabAndIndex ?? [undefined, undefined]; + // Figure out if active tab is positioned static which has an // impact on whether to reveal the tab or not later - let activeTabPositionStatic = this.accessor.partOptions.pinnedTabSizing !== 'normal' && this.group.isSticky(activeIndex); + let activeTabPositionStatic = this.accessor.partOptions.pinnedTabSizing !== 'normal' && typeof activeTabIndex === 'number' && this.tabsModel.isSticky(activeTabIndex); // Special case: we have sticky tabs but the available space for showing tabs // is little enough that we need to disable sticky tabs sticky positioning // so that tabs can be scrolled at naturally. let availableTabsContainerWidth = visibleTabsWidth - stickyTabsWidth; - if (this.group.stickyCount > 0 && availableTabsContainerWidth < MultiEditorTabsControl.TAB_WIDTH.fit) { + if (this.tabsModel.stickyCount > 0 && availableTabsContainerWidth < MultiEditorTabsControl.TAB_WIDTH.fit) { tabsContainer.classList.add('disable-sticky-tabs'); availableTabsContainerWidth = visibleTabsWidth; @@ -1806,7 +1828,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { let activeTabPosX: number | undefined; let activeTabWidth: number | undefined; - if (!this.blockRevealActiveTab) { + if (!this.blockRevealActiveTab && activeTab) { activeTabPosX = activeTab.offsetLeft; activeTabWidth = activeTab.offsetWidth; } @@ -1885,28 +1907,42 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } + private updateTabsControlVisibility(): void { + const tabsAndActionsContainer = assertIsDefined(this.tabsAndActionsContainer); + tabsAndActionsContainer.classList.toggle('empty', !this.visible); + + // Reset dimensions if hidden + if (!this.visible && this.dimensions) { + this.dimensions.used = undefined; + } + } + + private get visible(): boolean { + return this.tabsModel.count > 0; + } + private getTabAndIndex(editor: EditorInput): [HTMLElement, number /* index */] | undefined { - const editorIndex = this.group.getIndexOfEditor(editor); - const tab = this.getTabAtIndex(editorIndex); + const tabIndex = this.tabsModel.indexOf(editor); + const tab = this.getTabAtIndex(tabIndex); if (tab) { - return [tab, editorIndex]; + return [tab, tabIndex]; } return undefined; } - private getTabAtIndex(editorIndex: number): HTMLElement | undefined { - if (editorIndex >= 0) { + private getTabAtIndex(tabIndex: number): HTMLElement | undefined { + if (tabIndex >= 0) { const tabsContainer = assertIsDefined(this.tabsContainer); - return tabsContainer.children[editorIndex] as HTMLElement | undefined; + return tabsContainer.children[tabIndex] as HTMLElement | undefined; } return undefined; } private getLastTab(): HTMLElement | undefined { - return this.getTabAtIndex(this.group.count - 1); + return this.getTabAtIndex(this.tabsModel.count - 1); } private blockRevealActiveTabOnce(): void { @@ -1930,27 +1966,33 @@ export class MultiEditorTabsControl extends EditorTabsControl { return !!findParentWithClass(element, 'action-item', 'tab'); } - private async onDrop(e: DragEvent, targetIndex: number, tabsContainer: HTMLElement): Promise { + private async onDrop(e: DragEvent, targetTabIndex: number, tabsContainer: HTMLElement): Promise { EventHelper.stop(e, true); this.updateDropFeedback(tabsContainer, false); tabsContainer.classList.remove('scroll'); + const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupViewer.stickyCount : targetTabIndex; + const options: IEditorOptions = { + sticky: this.tabsModel instanceof StickyEditorGroupModel && this.tabsModel.stickyCount === targetEditorIndex, + index: targetEditorIndex + }; + // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { const sourceGroup = this.accessor.getGroup(data[0].identifier); if (sourceGroup) { - const mergeGroupOptions: IMergeGroupOptions = { index: targetIndex }; + const mergeGroupOptions: IMergeGroupOptions = { index: targetEditorIndex }; if (!this.isMoveOperation(e, sourceGroup.id)) { mergeGroupOptions.mode = MergeGroupMode.COPY_EDITORS; } - this.accessor.mergeGroup(sourceGroup, this.group, mergeGroupOptions); + this.accessor.mergeGroup(sourceGroup, this.groupViewer, mergeGroupOptions); } - this.group.focus(); + this.groupViewer.focus(); this.groupTransfer.clearData(DraggedEditorGroupIdentifier.prototype); } } @@ -1965,16 +2007,16 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Move editor to target position and index if (this.isMoveOperation(e, draggedEditor.groupId, draggedEditor.editor)) { - sourceGroup.moveEditor(draggedEditor.editor, this.group, { index: targetIndex }); + sourceGroup.moveEditor(draggedEditor.editor, this.groupViewer, options); } // Copy editor to target position and index else { - sourceGroup.copyEditor(draggedEditor.editor, this.group, { index: targetIndex }); + sourceGroup.copyEditor(draggedEditor.editor, this.groupViewer, options); } } - this.group.focus(); + this.groupViewer.focus(); this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } } @@ -1988,11 +2030,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { const dataTransferItem = await this.treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier); if (dataTransferItem) { const treeDropData = await extractTreeDropData(dataTransferItem); - editors.push(...treeDropData.map(editor => ({ ...editor, options: { ...editor.options, pinned: true, index: targetIndex } }))); + editors.push(...treeDropData.map(editor => ({ ...editor, options: { ...editor.options, pinned: true, index: targetEditorIndex } }))); } } - this.editorService.openEditors(editors, this.group, { validateTrust: true }); + this.editorService.openEditors(editors, this.groupViewer, { validateTrust: true }); } this.treeItemsTransfer.clearData(DraggedTreeItemsIdentifier.prototype); @@ -2001,7 +2043,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for URI transfer else { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false }); - dropHandler.handleDrop(e, () => this.group, () => this.group.focus(), targetIndex); + dropHandler.handleDrop(e, () => this.groupViewer, () => this.groupViewer.focus(), options); } } @@ -2012,7 +2054,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh); - return !isCopy || sourceGroup === this.group.id; + return (!isCopy || sourceGroup === this.groupViewer.id); } override dispose(): void { diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts new file mode 100644 index 00000000000..ef552ff9a96 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension } from 'vs/base/browser/dom'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; +import { MultiEditorTabsControl } from 'vs/workbench/browser/parts/editor/multiEditorTabsControl'; +import { IEditorPartOptions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { StickyEditorGroupModel, UnstickyEditorGroupModel } from 'vs/workbench/common/editor/filteredEditorGroupModel'; +import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor/editorTitleControl'; +import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; + +export class MultiRowEditorControl extends Disposable implements IEditorTabsControl { + + private readonly stickyEditorTabsControl: IEditorTabsControl; + private readonly unstickyEditorTabsControl: IEditorTabsControl; + + constructor( + private parent: HTMLElement, + private accessor: IEditorGroupsAccessor, + private groupViewer: IEditorGroupView, + private model: IReadonlyEditorGroupModel, + @IInstantiationService protected instantiationService: IInstantiationService + ) { + super(); + + const stickyModel = this._register(new StickyEditorGroupModel(this.model)); + const unstickyModel = this._register(new UnstickyEditorGroupModel(this.model)); + + this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.groupViewer, stickyModel)); + this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.groupViewer, unstickyModel)); + + this.handlePinnedTabsSeparateRowToolbars(); + } + + private handlePinnedTabsSeparateRowToolbars(): void { + if (this.groupViewer.count === 0) { + // Do nothing as no tab bar is visible + return; + } + // Ensure action toolbar is only visible once + if (this.groupViewer.count === this.groupViewer.stickyCount) { + this.parent.classList.toggle('two-tab-bars', false); + } else { + this.parent.classList.toggle('two-tab-bars', true); + } + } + + private getEditorTabsController(editor: EditorInput): IEditorTabsControl { + return this.model.isSticky(editor) ? this.stickyEditorTabsControl : this.unstickyEditorTabsControl; + } + + openEditor(editor: EditorInput): boolean { + const [editorTabController, otherTabController] = this.model.isSticky(editor) ? [this.stickyEditorTabsControl, this.unstickyEditorTabsControl] : [this.unstickyEditorTabsControl, this.stickyEditorTabsControl]; + const didChange = editorTabController.openEditor(editor); + if (didChange) { + // HACK: To render all editor tabs on startup, otherwise only one row gets rendered + otherTabController.openEditors([]); + } + return didChange; + } + + openEditors(editors: EditorInput[]): boolean { + const stickyEditors = editors.filter(e => this.model.isSticky(e)); + const unstickyEditors = editors.filter(e => !this.model.isSticky(e)); + + const didChangeOpenEditorsSticky = this.stickyEditorTabsControl.openEditors(stickyEditors); + const didChangeOpenEditorsUnSticky = this.unstickyEditorTabsControl.openEditors(unstickyEditors); + + return didChangeOpenEditorsSticky || didChangeOpenEditorsUnSticky; + } + + beforeCloseEditor(editor: EditorInput): void { + this.getEditorTabsController(editor).beforeCloseEditor(editor); + } + + closeEditor(editor: EditorInput): void { + // Has to be called on both tab bars as the editor could be either sticky or not + this.stickyEditorTabsControl.closeEditor(editor); + this.unstickyEditorTabsControl.closeEditor(editor); + + this.handleClosedEditors(); + } + + closeEditors(editors: EditorInput[]): void { + const stickyEditors = editors.filter(e => this.model.isSticky(e)); + const unstickyEditors = editors.filter(e => !this.model.isSticky(e)); + + this.stickyEditorTabsControl.closeEditors(stickyEditors); + this.unstickyEditorTabsControl.closeEditors(unstickyEditors); + + this.handleClosedEditors(); + } + + private handleClosedEditors(): void { + this.handlePinnedTabsSeparateRowToolbars(); + } + + moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void { + if (stickyStateChange) { + // If sticky state changes, move editor between tab bars + if (this.model.isSticky(editor)) { + this.stickyEditorTabsControl.openEditor(editor); + this.unstickyEditorTabsControl.closeEditor(editor); + } else { + this.stickyEditorTabsControl.closeEditor(editor); + this.unstickyEditorTabsControl.openEditor(editor); + } + + this.handlePinnedTabsSeparateRowToolbars(); + + } else { + if (this.model.isSticky(editor)) { + this.stickyEditorTabsControl.moveEditor(editor, fromIndex, targetIndex, stickyStateChange); + } else { + this.unstickyEditorTabsControl.moveEditor(editor, fromIndex - this.model.stickyCount, targetIndex - this.model.stickyCount, stickyStateChange); + } + } + } + + pinEditor(editor: EditorInput): void { + this.getEditorTabsController(editor).pinEditor(editor); + } + + stickEditor(editor: EditorInput): void { + this.unstickyEditorTabsControl.closeEditor(editor); + this.stickyEditorTabsControl.openEditor(editor); + + this.handlePinnedTabsSeparateRowToolbars(); + } + + unstickEditor(editor: EditorInput): void { + this.stickyEditorTabsControl.closeEditor(editor); + this.unstickyEditorTabsControl.openEditor(editor); + + this.handlePinnedTabsSeparateRowToolbars(); + } + + setActive(isActive: boolean): void { + this.stickyEditorTabsControl.setActive(isActive); + this.unstickyEditorTabsControl.setActive(isActive); + } + + updateEditorLabel(editor: EditorInput): void { + this.getEditorTabsController(editor).updateEditorLabel(editor); + } + + updateEditorDirty(editor: EditorInput): void { + this.getEditorTabsController(editor).updateEditorDirty(editor); + } + + updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { + this.stickyEditorTabsControl.updateOptions(oldOptions, newOptions); + this.unstickyEditorTabsControl.updateOptions(oldOptions, newOptions); + } + + layout(dimensions: IEditorTitleControlDimensions): Dimension { + const stickyDimensions = this.stickyEditorTabsControl.layout(dimensions); + const unstickyDimensions = this.unstickyEditorTabsControl.layout(dimensions); + + return new Dimension( + dimensions.container.width, + stickyDimensions.height + unstickyDimensions.height + ); + } + + getHeight(): number { + return this.stickyEditorTabsControl.getHeight() + this.unstickyEditorTabsControl.getHeight(); + } + + public override dispose(): void { + this.parent.classList.toggle('two-tab-bars', false); + + super.dispose(); + } +} diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index 9912e00a295..5cf2d6e27ef 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -55,7 +55,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { this._register(addDisposableListener(this.editorLabel.element, EventType.CLICK, e => this.onTitleLabelClick(e))); // Breadcrumbs - this.breadcrumbsControlFactory = this._register(this.instantiationService.createInstance(BreadcrumbsControlFactory, labelContainer, this.group, { + this.breadcrumbsControlFactory = this._register(this.instantiationService.createInstance(BreadcrumbsControlFactory, labelContainer, this.groupViewer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, @@ -92,8 +92,8 @@ export class SingleEditorTabsControl extends EditorTabsControl { // Context Menu for (const event of [EventType.CONTEXT_MENU, TouchEventType.Contextmenu]) { this._register(addDisposableListener(titleContainer, event, e => { - if (this.group.activeEditor) { - this.onContextMenu(this.group.activeEditor, e, titleContainer); + if (this.tabsModel.activeEditor) { + this.onTabContextMenu(this.tabsModel.activeEditor, e, titleContainer); } })); } @@ -109,15 +109,15 @@ export class SingleEditorTabsControl extends EditorTabsControl { private onTitleDoubleClick(e: MouseEvent): void { EventHelper.stop(e); - this.group.pinEditor(); + this.groupViewer.pinEditor(); } private onTitleAuxClick(e: MouseEvent): void { - if (e.button === 1 /* Middle Button */ && this.group.activeEditor) { + if (e.button === 1 /* Middle Button */ && this.tabsModel.activeEditor) { EventHelper.stop(e, true /* for https://github.com/microsoft/vscode/issues/56715 */); - if (!preventEditorClose(this.group, this.group.activeEditor, EditorCloseMethod.MOUSE, this.accessor.partOptions)) { - this.group.closeEditor(this.group.activeEditor); + if (!preventEditorClose(this.tabsModel, this.tabsModel.activeEditor, EditorCloseMethod.MOUSE, this.accessor.partOptions)) { + this.groupViewer.closeEditor(this.tabsModel.activeEditor); } } } @@ -231,9 +231,9 @@ export class SingleEditorTabsControl extends EditorTabsControl { private ifActiveEditorChanged(fn: () => void): boolean { if ( - !this.activeLabel.editor && this.group.activeEditor || // active editor changed from null => editor - this.activeLabel.editor && !this.group.activeEditor || // active editor changed from editor => null - (!this.activeLabel.editor || !this.group.isActive(this.activeLabel.editor)) // active editor changed from editorA => editorB + !this.activeLabel.editor && this.tabsModel.activeEditor || // active editor changed from null => editor + this.activeLabel.editor && !this.tabsModel.activeEditor || // active editor changed from editor => null + (!this.activeLabel.editor || !this.tabsModel.isActive(this.activeLabel.editor)) // active editor changed from editorA => editorB ) { fn(); @@ -244,27 +244,27 @@ export class SingleEditorTabsControl extends EditorTabsControl { } private ifActiveEditorPropertiesChanged(fn: () => void): void { - if (!this.activeLabel.editor || !this.group.activeEditor) { + if (!this.activeLabel.editor || !this.tabsModel.activeEditor) { return; // need an active editor to check for properties changed } - if (this.activeLabel.pinned !== this.group.isPinned(this.group.activeEditor)) { + if (this.activeLabel.pinned !== this.tabsModel.isPinned(this.tabsModel.activeEditor)) { fn(); // only run if pinned state has changed } } private ifEditorIsActive(editor: EditorInput, fn: () => void): void { - if (this.group.isActive(editor)) { + if (this.tabsModel.isActive(editor)) { fn(); // only run if editor is current active } } private redraw(): void { - const editor = this.group.activeEditor ?? undefined; + const editor = this.tabsModel.activeEditor ?? undefined; const options = this.accessor.partOptions; - const isEditorPinned = editor ? this.group.isPinned(editor) : false; - const isGroupActive = this.accessor.activeGroup === this.group; + const isEditorPinned = editor ? this.tabsModel.isPinned(editor) : false; + const isGroupActive = this.accessor.activeGroup === this.groupViewer; this.activeLabel = { editor, pinned: isEditorPinned }; @@ -345,7 +345,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { } protected override prepareEditorActions(editorActions: IToolbarActions): IToolbarActions { - const isGroupActive = this.accessor.activeGroup === this.group; + const isGroupActive = this.accessor.activeGroup === this.groupViewer; // Active: allow all actions if (isGroupActive) { diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 30a4e589a06..d3d132cb2ed 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -7,6 +7,7 @@ import { reset } from 'vs/base/browser/dom'; import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; @@ -182,6 +183,16 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { }); toolbar.setActions(group); this._store.add(toolbar); + + + // spacer + if (i < groups.length - 1) { + const icon = renderIcon(Codicon.circleSmallFilled); + icon.style.padding = '0 12px'; + icon.style.height = '100%'; + icon.style.opacity = '0.5'; + container.appendChild(icon); + } } } diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 44a1a6aa908..c78ebf86e1f 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -179,7 +179,7 @@ } .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple { - justify-content: space-between; + justify-content: flex-start; padding: 0 12px; } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 83aa1e53241..88eecdc0c9a 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -182,6 +182,11 @@ const registry = Registry.as(ConfigurationExtensions.Con ], 'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the size of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, + 'workbench.editor.pinnedTabsOnSeparateRow': { + 'type': 'boolean', + 'default': false, + 'markdownDescription': localize('workbench.editor.pinnedTabsOnSeparateRow', "When enabled, displays pinned tabs in a separate row above all other tabs. This value is ignored when `#workbench.editor.showTabs#` is disabled."), + }, 'workbench.editor.preventPinnedEditorClose': { 'type': 'string', 'enum': ['keyboardAndMouse', 'keyboard', 'mouse', 'never'], diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index d38c979a477..c64a144dcde 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -27,6 +27,7 @@ import { IErrorWithActions, createErrorWithActions, isErrorWithActions } from 'v import { IAction, toAction } from 'vs/base/common/actions'; import Severity from 'vs/base/common/severity'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; // Static values for editor contributions export const EditorExtensions = { @@ -1098,6 +1099,7 @@ interface IEditorPartConfiguration { tabSizingFixedMinWidth?: number; tabSizingFixedMaxWidth?: number; pinnedTabSizing?: 'normal' | 'compact' | 'shrink'; + pinnedTabsOnSeparateRow?: boolean; tabHeight?: 'normal' | 'compact'; preventPinnedEditorClose?: PreventPinnedEditorClose; titleScrollbarSizing?: 'default' | 'large'; @@ -1350,7 +1352,7 @@ export enum EditorCloseMethod { MOUSE } -export function preventEditorClose(group: IEditorGroup, editor: EditorInput, method: EditorCloseMethod, configuration: IEditorPartConfiguration): boolean { +export function preventEditorClose(group: IEditorGroup | IReadonlyEditorGroupModel, editor: EditorInput, method: EditorCloseMethod, configuration: IEditorPartConfiguration): boolean { if (!group.isSticky(editor)) { return false; // only interested in sticky editors } diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 8e3c7b7a811..e394038fb85 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -163,7 +163,37 @@ interface IEditorCloseResult { readonly sticky: boolean; } -export class EditorGroupModel extends Disposable { +export interface IReadonlyEditorGroupModel { + + readonly onDidModelChange: Event; + + readonly id: GroupIdentifier; + readonly count: number; + readonly stickyCount: number; + readonly isLocked: boolean; + readonly activeEditor: EditorInput | null; + readonly previewEditor: EditorInput | null; + + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[]; + getEditorByIndex(index: number): EditorInput | undefined; + indexOf(editor: EditorInput | IUntypedEditorInput | null, editors?: EditorInput[], options?: IMatchEditorOptions): number; + isActive(editor: EditorInput | IUntypedEditorInput): boolean; + isPinned(editorOrIndex: EditorInput | number): boolean; + isSticky(editorOrIndex: EditorInput | number): boolean; + isFirst(editor: EditorInput): boolean; + isLast(editor: EditorInput): boolean; + findEditor(editor: EditorInput | null, options?: IMatchEditorOptions): [EditorInput, number /* index */] | undefined; + contains(editor: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean; +} + +interface IEditorGroupModel extends IReadonlyEditorGroupModel { + openEditor(editor: EditorInput, options?: IEditorOpenOptions): IEditorOpenResult; + closeEditor(editor: EditorInput, context?: EditorCloseContext, openNext?: boolean): IEditorCloseResult | undefined; + moveEditor(editor: EditorInput, toIndex: number): EditorInput | undefined; + setActive(editor: EditorInput | undefined): EditorInput | undefined; +} + +export class EditorGroupModel extends Disposable implements IEditorGroupModel { private static IDS = 0; diff --git a/src/vs/workbench/common/editor/editorInput.ts b/src/vs/workbench/common/editor/editorInput.ts index 5cce6162694..f2885bc871d 100644 --- a/src/vs/workbench/common/editor/editorInput.ts +++ b/src/vs/workbench/common/editor/editorInput.ts @@ -69,8 +69,6 @@ export abstract class EditorInput extends AbstractEditorInput { */ readonly onWillDispose = this._onWillDispose.event; - private disposed: boolean = false; - /** * Optional: subclasses can override to implement * custom confirmation on close behavior. @@ -316,12 +314,11 @@ export abstract class EditorInput extends AbstractEditorInput { * Returns if this editor is disposed. */ isDisposed(): boolean { - return this.disposed; + return this._store.isDisposed; } override dispose(): void { - if (!this.disposed) { - this.disposed = true; + if (!this.isDisposed()) { this._onWillDispose.fire(); } diff --git a/src/vs/workbench/common/editor/editorModel.ts b/src/vs/workbench/common/editor/editorModel.ts index ae5ba0d1cc3..74cc76dfb5b 100644 --- a/src/vs/workbench/common/editor/editorModel.ts +++ b/src/vs/workbench/common/editor/editorModel.ts @@ -17,7 +17,6 @@ export class EditorModel extends Disposable implements IEditorModel { private readonly _onWillDispose = this._register(new Emitter()); readonly onWillDispose = this._onWillDispose.event; - private disposed = false; private resolved = false; /** @@ -38,14 +37,13 @@ export class EditorModel extends Disposable implements IEditorModel { * Find out if this model has been disposed. */ isDisposed(): boolean { - return this.disposed; + return this._store.isDisposed; } /** * Subclasses should implement to free resources that have been claimed through loading. */ override dispose(): void { - this.disposed = true; this._onWillDispose.fire(); super.dispose(); diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts new file mode 100644 index 00000000000..a8dc47321d1 --- /dev/null +++ b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUntypedEditorInput, IMatchEditorOptions, EditorsOrder, GroupIdentifier } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { Emitter } from 'vs/base/common/event'; +import { IGroupModelChangeEvent, IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; +import { Disposable } from 'vs/base/common/lifecycle'; + +abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyEditorGroupModel { + + private readonly _onDidModelChange = this._register(new Emitter()); + readonly onDidModelChange = this._onDidModelChange.event; + + constructor( + protected readonly model: IReadonlyEditorGroupModel + ) { + super(); + + this._register(this.model.onDidModelChange(e => { + const candidateOrIndex = e.editorIndex ?? e.editor; + if (candidateOrIndex !== undefined) { + if (!this.filter(candidateOrIndex)) { + return; // exclude events for excluded items + } + } + this._onDidModelChange.fire(e); + })); + } + + get id(): GroupIdentifier { return this.model.id; } + get isLocked(): boolean { return this.model.isLocked; } + get stickyCount(): number { return this.model.stickyCount; } + + get activeEditor(): EditorInput | null { return this.model.activeEditor && this.filter(this.model.activeEditor) ? this.model.activeEditor : null; } + get previewEditor(): EditorInput | null { return this.model.previewEditor && this.filter(this.model.previewEditor) ? this.model.previewEditor : null; } + + isPinned(editorOrIndex: EditorInput | number): boolean { return this.model.isPinned(editorOrIndex); } + isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } + isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } + + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[] { + const editors = this.model.getEditors(order, options); + return editors.filter(e => this.filter(e)); + } + + findEditor(candidate: EditorInput | null, options?: IMatchEditorOptions): [EditorInput, number] | undefined { + const result = this.model.findEditor(candidate, options); + if (!result) { + return undefined; + } + return this.filter(result[1]) ? result : undefined; + } + + abstract get count(): number; + + abstract isFirst(editor: EditorInput): boolean; + abstract isLast(editor: EditorInput): boolean; + abstract getEditorByIndex(index: number): EditorInput | undefined; + abstract indexOf(editor: EditorInput | IUntypedEditorInput | null, editors?: EditorInput[], options?: IMatchEditorOptions): number; + abstract contains(editor: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean; + + protected abstract filter(editorOrIndex: EditorInput | number): boolean; +} + +export class StickyEditorGroupModel extends FilteredEditorGroupModel { + get count(): number { return this.model.stickyCount; } + + override getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[] { + if (options?.excludeSticky) { + return []; + } + if (order === EditorsOrder.SEQUENTIAL) { + return this.model.getEditors(EditorsOrder.SEQUENTIAL).slice(0, this.model.stickyCount); + } + return super.getEditors(order, options); + } + + override isSticky(editorOrIndex: number | EditorInput): boolean { + return true; + } + + isFirst(editor: EditorInput): boolean { + return this.model.isFirst(editor); + } + + isLast(editor: EditorInput): boolean { + return this.model.indexOf(editor) === this.model.stickyCount - 1; + } + + getEditorByIndex(index: number): EditorInput | undefined { + return index < this.count ? this.model.getEditorByIndex(index) : undefined; + } + + indexOf(editor: EditorInput | IUntypedEditorInput | null, editors?: EditorInput[], options?: IMatchEditorOptions): number { + const editorIndex = this.model.indexOf(editor, editors, options); + if (editorIndex < 0 || editorIndex >= this.model.stickyCount) { + return -1; + } + return editorIndex; + } + + contains(candidate: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean { + const editorIndex = this.model.indexOf(candidate, undefined, options); + return editorIndex >= 0 && editorIndex < this.model.stickyCount; + } + + protected filter(candidateOrIndex: EditorInput | number): boolean { + return this.model.isSticky(candidateOrIndex); + } +} + +export class UnstickyEditorGroupModel extends FilteredEditorGroupModel { + get count(): number { return this.model.count - this.model.stickyCount; } + override get stickyCount(): number { return 0; } + + override isSticky(editorOrIndex: number | EditorInput): boolean { + return false; + } + + override getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[] { + if (order === EditorsOrder.SEQUENTIAL) { + return this.model.getEditors(EditorsOrder.SEQUENTIAL).slice(this.model.stickyCount); + } + return super.getEditors(order, options); + } + + isFirst(editor: EditorInput): boolean { + return this.model.indexOf(editor) === this.model.stickyCount; + } + + isLast(editor: EditorInput): boolean { + return this.model.isLast(editor); + } + + getEditorByIndex(index: number): EditorInput | undefined { + return index >= 0 ? this.model.getEditorByIndex(index + this.model.stickyCount) : undefined; + } + + indexOf(editor: EditorInput | IUntypedEditorInput | null, editors?: EditorInput[], options?: IMatchEditorOptions): number { + const editorIndex = this.model.indexOf(editor, editors, options); + if (editorIndex < this.model.stickyCount || editorIndex >= this.model.count) { + return -1; + } + return editorIndex - this.model.stickyCount; + } + + contains(candidate: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean { + const editorIndex = this.model.indexOf(candidate, undefined, options); + return editorIndex >= this.model.stickyCount && editorIndex < this.model.count; + } + + protected filter(candidateOrIndex: EditorInput | number): boolean { + return !this.model.isSticky(candidateOrIndex); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index f9c312fa4ce..00e3979d1ff 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import { Iterable } from 'vs/base/common/iterator'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Position } from 'vs/editor/common/core/position'; @@ -16,6 +15,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -26,6 +26,8 @@ import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart } from '../../common/chatRequestParser'; +import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -42,11 +44,10 @@ class InputEditorDecorations extends Disposable { constructor( private readonly widget: IChatWidget, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IThemeService private readonly themeService: IThemeService, @IChatService private readonly chatService: IChatService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -94,7 +95,11 @@ class InputEditorDecorations extends Disposable { private async updateInputEditorDecorations() { const inputValue = this.widget.inputEditor.getValue(); const slashCommands = await this.widget.getSlashCommands(); // TODO this async call can lead to a flicker of the placeholder text when switching editor tabs - const agents = this.chatAgentService.getAgents(); + + const viewModel = this.widget.viewModel; + if (!viewModel) { + return; + } if (!inputValue) { const extensionPlaceholder = this.widget.viewModel?.inputPlaceholder; @@ -122,39 +127,27 @@ class InputEditorDecorations extends Disposable { return; } - // TODO@roblourens need some kind of parser for queries + const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(viewModel.sessionId, inputValue); let placeholderDecoration: IDecorationOptions[] | undefined; - const usedAgent = inputValue && agents.find(a => inputValue.startsWith(`@${a.id} `)); + const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + const agentSubcommandPart = parsedRequest.find((p): p is ChatRequestAgentSubcommandPart => p instanceof ChatRequestAgentSubcommandPart); + const slashCommandPart = parsedRequest.find((p): p is ChatRequestSlashCommandPart => p instanceof ChatRequestSlashCommandPart); - let usedSubcommand: string | undefined; - let subCommandPosition: number | undefined; - if (usedAgent) { - const subCommandReg = /\/(\w+)(\s|$)/g; - let subCommandMatch: RegExpExecArray | null; - while (subCommandMatch = subCommandReg.exec(inputValue)) { - const maybeCommand = subCommandMatch[1]; - usedSubcommand = usedAgent.metadata.subCommands.find(agentCommand => maybeCommand === agentCommand.name)?.name; - if (usedSubcommand) { - subCommandPosition = subCommandMatch.index; - break; - } - } - } - - if (usedAgent && inputValue === `@${usedAgent.id} `) { + const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart); + if (onlyAgentAndWhitespace) { // Agent reference with no other text - show the placeholder - if (usedAgent.metadata.description) { + if (agentPart.agent.metadata.description) { placeholderDecoration = [{ range: { startLineNumber: 1, endLineNumber: 1, - startColumn: usedAgent.id.length, + startColumn: inputValue.length, endColumn: 1000 }, renderOptions: { after: { - contentText: usedAgent.metadata.description, + contentText: agentPart.agent.metadata.description, color: this.getPlaceholderColor(), } } @@ -162,22 +155,22 @@ class InputEditorDecorations extends Disposable { } } - const command = !usedAgent && inputValue && slashCommands?.find(c => inputValue.startsWith(`/${c.command} `)); - if (command && inputValue === `/${command.command} `) { + const onlySlashCommandAndWhitespace = slashCommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestSlashCommandPart); + if (onlySlashCommandAndWhitespace) { // Command reference with no other text - show the placeholder - const isFollowupSlashCommand = this._previouslyUsedSlashCommands.has(command.command); - const shouldRenderFollowupPlaceholder = command.followupPlaceholder && isFollowupSlashCommand; - if (shouldRenderFollowupPlaceholder || command.detail) { + const isFollowupSlashCommand = this._previouslyUsedSlashCommands.has(slashCommandPart.slashCommand.command); + const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && slashCommandPart.slashCommand.followupPlaceholder; + if (shouldRenderFollowupPlaceholder || slashCommandPart.slashCommand.detail) { placeholderDecoration = [{ range: { startLineNumber: 1, endLineNumber: 1, - startColumn: command ? command.command.length : 1, + startColumn: inputValue.length, endColumn: 1000 }, renderOptions: { after: { - contentText: shouldRenderFollowupPlaceholder ? command.followupPlaceholder : command.detail, + contentText: shouldRenderFollowupPlaceholder ? slashCommandPart.slashCommand.followupPlaceholder : slashCommandPart.slashCommand.detail, color: this.getPlaceholderColor(), } } @@ -187,64 +180,24 @@ class InputEditorDecorations extends Disposable { this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, placeholderDecoration ?? []); - // TODO@roblourens The way these numbers are computed aren't totally correct... const textDecorations: IDecorationOptions[] | undefined = []; - if (usedAgent) { - textDecorations.push( - { - range: { - startLineNumber: 1, - endLineNumber: 1, - startColumn: 1, - endColumn: usedAgent.id.length + 2 - } - } - ); - if (usedSubcommand) { - textDecorations.push( - { - range: { - startLineNumber: 1, - endLineNumber: 1, - startColumn: subCommandPosition! + 1, - endColumn: subCommandPosition! + usedSubcommand.length + 2 - } - } - ); + if (agentPart) { + textDecorations.push({ range: agentPart.editorRange }); + if (agentSubcommandPart) { + textDecorations.push({ range: agentSubcommandPart.editorRange }); } } - if (command) { - textDecorations.push( - { - range: { - startLineNumber: 1, - endLineNumber: 1, - startColumn: 1, - endColumn: command.command.length + 2 - } - } - ); + if (slashCommandPart) { + textDecorations.push({ range: slashCommandPart.editorRange }); } this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations); - const variables = this.chatVariablesService.getVariables(); - const variableReg = /(^|\s)@(\w+)(:\d+)?(?=(\s|$))/ig; - let match: RegExpMatchArray | null; const varDecorations: IDecorationOptions[] = []; - while (match = variableReg.exec(inputValue)) { - const varName = match[2]; - if (Iterable.find(variables, v => v.name === varName)) { - varDecorations.push({ - range: { - startLineNumber: 1, - endLineNumber: 1, - startColumn: match.index! + match[1].length + 1, - endColumn: match.index! + match[0].length + 1 - } - }); - } + const variableParts = parsedRequest.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); + for (const variable of variableParts) { + varDecorations.push({ range: variable.editorRange }); } this.widget.inputEditor.setDecorationsByType(decorationDescription, variableTextDecorationType, varDecorations); @@ -286,7 +239,7 @@ class SlashCommandCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -295,23 +248,17 @@ class SlashCommandCompletions extends Disposable { triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, _position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget) { + if (!widget || !widget.viewModel) { return null; } - const firstLine = model.getLineContent(1).trim(); - - const agents = this.chatAgentService.getAgents(); - const usedAgent = firstLine.startsWith('@') && agents.find(a => firstLine.startsWith(`@${a.id}`)); + const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); if (usedAgent) { // No (classic) global slash commands when an agent is used return; } - if (model.getValueInRange(new Range(1, 1, 1, 2)) !== '/' && model.getValueLength() > 0) { - return null; - } - const slashCommands = await widget.getSlashCommands(); if (!slashCommands) { return null; @@ -342,20 +289,29 @@ class AgentCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatAgentService private readonly chatAgentService: IChatAgentService + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'chatAgent', triggerCharacters: ['@'], - provideCompletionItems: async (model: ITextModel, _position: Position, _context: CompletionContext, _token: CancellationToken) => { + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget) { + if (!widget || !widget.viewModel) { return null; } - if (model.getValueInRange(new Range(1, 1, 1, 2)) !== '@' && model.getValueLength() > 0) { + const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { + // Only one agent allowed + return; + } + + const range = computeCompletionRanges(model, position, /@\w*/g); + if (!range) { return null; } @@ -367,10 +323,8 @@ class AgentCompletions extends Disposable { label: withAt, insertText: `${withAt} `, detail: c.metadata.description, - range: new Range(1, 1, 1, 1), - // sortText: 'a'.repeat(i + 1), + range, kind: CompletionItemKind.Text, // The icons are disabled here anyway - // command: c.executeImmediately ? { id: SubmitAction.ID, title: withAt, arguments: [{ widget, inputValue: `${withAt} ` }] } : undefined, }; }) }; @@ -382,40 +336,31 @@ class AgentCompletions extends Disposable { triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget) { + if (!widget || !widget.viewModel) { return; } - const firstLine = model.getLineContent(1).trim(); - - if (!firstLine.startsWith('@')) { - return; - } - - const agents = this.chatAgentService.getAgents(); - const usedAgent = agents.find(a => firstLine.startsWith(`@${a.id}`)); + const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const usedAgent = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); if (!usedAgent) { return; } - const maybeCommands = model.getValue().split(/\s+/).filter(w => w.startsWith('/')); - const usedSubcommand = usedAgent.metadata.subCommands.find(agentCommand => maybeCommands.some(c => c === `/${agentCommand.name}`)); + const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); if (usedSubcommand) { // Only one allowed return; } return { - suggestions: usedAgent.metadata.subCommands.map((c, i) => { + suggestions: usedAgent.agent.metadata.subCommands.map((c, i) => { const withSlash = `/${c.name}`; return { label: withSlash, insertText: `${withSlash} `, detail: c.description, range: new Range(1, position.column - 1, 1, position.column - 1), - // sortText: 'a'.repeat(i + 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway - // command: c.executeImmediately ? { id: SubmitAction.ID, title: withAt, arguments: [{ widget, inputValue: `${withAt} ` }] } : undefined, }; }) }; @@ -530,6 +475,25 @@ function sortSlashCommandsByYieldTo(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); +function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range } | undefined { + const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); + if (!varWord && model.getWordUntilPosition(position).word) { + // inside a "normal" word + return; + } + + let insert: Range; + let replace: Range; + if (!varWord) { + insert = replace = Range.fromPositions(position); + } else { + insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); + replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); + } + + return { insert, replace }; +} + class VariableCompletions extends Disposable { private static readonly VariableNameDef = /@\w*/g; // MUST be using `g`-flag @@ -552,21 +516,11 @@ class VariableCompletions extends Disposable { return null; } - const varWord = getWordAtText(position.column, VariableCompletions.VariableNameDef, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - // inside a "normal" word + const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); + if (!range) { return null; } - let insert: Range; - let replace: Range; - if (!varWord) { - insert = replace = Range.fromPositions(position); - } else { - insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); - replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); - } - const history = widget.viewModel!.getItems() .filter(isResponseVM); @@ -577,14 +531,14 @@ class VariableCompletions extends Disposable { detail: h.response.asString(), insertText: `@response:${String(i + 1).padStart(String(history.length).length, '0')} `, kind: CompletionItemKind.Text, - range: { insert, replace }, + range, })) : []; const variableItems = Array.from(this.chatVariablesService.getVariables()).map(v => { const withAt = `@${v.name}`; return { label: withAt, - range: { insert, replace }, + range, insertText: withAt + ' ', detail: v.description, kind: CompletionItemKind.Text, // The icons are disabled here anyway, diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index e7e4c4cfa83..dcf7c2d4f9e 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -98,6 +98,7 @@ export interface IChatAgentService { registerAgent(data: IChatAgentData, callback: IChatAgentCallback): IDisposable; invokeAgent(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; getAgents(): Array; + getAgent(id: string): IChatAgentData | undefined; hasAgent(id: string): boolean; } @@ -161,6 +162,11 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return this._agents.has(id); } + getAgent(id: string): IChatAgentData | undefined { + const data = this._agents.get(id); + return data?.data; + } + async invokeAgent(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { const data = this._agents.get(id); if (!data) { diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts new file mode 100644 index 00000000000..b6ff47c6680 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; + +const variableOrAgentReg = /^@([\w_\-]+)(:\d+)?(?=(\s|$))/i; // An @-variable with an optional numeric : arg (@response:2) +const slashReg = /\/([\w_-]+)(?=(\s|$))/i; // A / command + +export class ChatRequestParser { + constructor( + @IChatAgentService private readonly agentService: IChatAgentService, + @IChatVariablesService private readonly variableService: IChatVariablesService, + @IChatService private readonly chatService: IChatService, + ) { } + + async parseChatRequest(sessionId: string, message: string): Promise { + const parts: IParsedChatRequestPart[] = []; + + let lineNumber = 1; + let column = 1; + for (let i = 0; i < message.length; i++) { + const previousChar = message.charAt(i - 1); + const char = message.charAt(i); + let newPart: IParsedChatRequestPart | undefined; + if (char === '@' && (previousChar === ' ' || i === 0)) { + newPart = this.tryToParseVariableOrAgent(message.slice(i), i, new Position(lineNumber, column), parts); + } else if (char === '/' && (previousChar === ' ' || i === 0)) { + // TODO try to make this sync + newPart = await this.tryToParseSlashCommand(sessionId, message.slice(i), i, new Position(lineNumber, column), parts); + } + + if (newPart) { + if (i !== 0) { + // Insert a part for all the text we passed over, then insert the new parsed part + const previousPart = parts.at(-1); + const previousPartEnd = previousPart?.range.endExclusive ?? 0; + const previousPartEditorRangeEndLine = previousPart?.editorRange.endLineNumber ?? 1; + const previousPartEditorRangeEndCol = previousPart?.editorRange.endColumn ?? 1; + parts.push(new ChatRequestTextPart( + new OffsetRange(previousPartEnd, i), + new Range(previousPartEditorRangeEndLine, previousPartEditorRangeEndCol, lineNumber, column), + message.slice(previousPartEnd, i))); + } + + parts.push(newPart); + } + + if (char === '\n') { + lineNumber++; + column = 1; + } else { + column++; + } + } + + const lastPart = parts.at(-1); + const lastPartEnd = lastPart?.range.endExclusive ?? 0; + parts.push(new ChatRequestTextPart( + new OffsetRange(lastPartEnd, message.length), + new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column), + message.slice(lastPartEnd, message.length))); + + return parts; + } + + private tryToParseVariableOrAgent(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { + const nextVariableMatch = message.match(variableOrAgentReg); + if (!nextVariableMatch) { + return; + } + + const [full, name] = nextVariableMatch; + const variableArg = nextVariableMatch[2] ?? ''; + const varRange = new OffsetRange(offset, offset + full.length); + const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + + let agent: IChatAgentData | undefined; + if ((agent = this.agentService.getAgent(name)) && !variableArg) { + if (parts.some(p => p instanceof ChatRequestAgentPart)) { + // Only one agent allowed + return; + } else { + return new ChatRequestAgentPart(varRange, varEditorRange, agent); + } + } else if (this.variableService.hasVariable(name)) { + return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg); + } + + return; + } + + private async tryToParseSlashCommand(sessionId: string, message: string, offset: number, position: IPosition, parts: ReadonlyArray): Promise { + const nextSlashMatch = message.match(slashReg); + if (!nextSlashMatch) { + return; + } + + if (parts.some(p => p instanceof ChatRequestSlashCommandPart)) { + // Only one slash command allowed + return; + } + + const [full, command] = nextSlashMatch; + const slashRange = new OffsetRange(offset, offset + full.length); + const slashEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + + const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + if (usedAgent) { + const subCommand = usedAgent.agent.metadata.subCommands.find(c => c.name === command); + if (subCommand) { + // Valid agent subcommand + return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); + } + } else { + const slashCommands = await this.chatService.getSlashCommands(sessionId, CancellationToken.None); + const slashCommand = slashCommands.find(c => c.command === command); + if (slashCommand) { + // Valid standalone slash command + return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand); + } + } + + return; + } +} + +export interface IParsedChatRequestPart { + readonly range: OffsetRange; + readonly editorRange: IRange; +} + +export class ChatRequestTextPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string) { } +} +/** + * An invocation of a static variable that can be resolved by the variable service + */ + +export class ChatRequestVariablePart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } +} +/** + * An invocation of an agent that can be resolved by the agent service + */ + +export class ChatRequestAgentPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } +} +/** + * An invocation of an agent's subcommand + */ + +export class ChatRequestAgentSubcommandPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly command: IChatAgentCommand) { } +} +/** + * An invocation of a standalone slash command + */ + +export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashCommand: ISlashCommand) { } +} diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 39fb93a4739..b97c1a7c995 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -224,7 +224,7 @@ export interface IChatService { sendRequest(sessionId: string, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise<{ responseCompletePromise: Promise } | undefined>; removeRequest(sessionid: string, requestId: string): Promise; cancelCurrentRequestForSession(sessionId: string): void; - getSlashCommands(sessionId: string, token: CancellationToken): Promise; + getSlashCommands(sessionId: string, token: CancellationToken): Promise; clearSession(sessionId: string): void; addRequest(context: any): void; addCompleteRequest(sessionId: string, message: string, response: IChatCompleteResponse): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 8bdc8b21459..75bed122f24 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -633,7 +633,7 @@ export class ChatService extends Disposable implements IChatService { return agents.find(a => prompt.match(new RegExp(`@${a.id}($|\\s)`))); } - async getSlashCommands(sessionId: string, token: CancellationToken): Promise { + async getSlashCommands(sessionId: string, token: CancellationToken): Promise { const model = this._sessionModels.get(sessionId); if (!model) { throw new Error(`Unknown session: ${sessionId}`); diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 68bac81975f..0fb80a2b0ef 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -33,6 +33,7 @@ export const IChatVariablesService = createDecorator('ICh export interface IChatVariablesService { _serviceBrand: undefined; registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable; + hasVariable(name: string): boolean; getVariables(): Iterable>; /** @@ -102,6 +103,10 @@ export class ChatVariablesService implements IChatVariablesService { }; } + hasVariable(name: string): boolean { + return this._resolver.has(name.toLowerCase()); + } + getVariables(): Iterable> { const all = Iterable.map(this._resolver.values(), data => data.data); return Iterable.filter(all, data => !data.hidden); 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 new file mode 100644 index 00000000000..ca127ef833b --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__agents_and_variables_and_multiline_0.snap @@ -0,0 +1,60 @@ +[ + { + range: { + start: 0, + endExclusive: 6 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 7 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } + } + }, + { + range: { + start: 6, + endExclusive: 18 + }, + editorRange: { + startLineNumber: 1, + startColumn: 7, + endLineNumber: 2, + endColumn: 4 + }, + text: " Please \ndo " + }, + { + range: { + start: 18, + endExclusive: 29 + }, + editorRange: { + startLineNumber: 2, + startColumn: 4, + endLineNumber: 2, + endColumn: 15 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 29, + endExclusive: 63 + }, + editorRange: { + startLineNumber: 2, + startColumn: 15, + endLineNumber: 3, + endColumn: 18 + }, + text: " with @selection\nand @debugConsole" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines_0.snap new file mode 100644 index 00000000000..31b7d3be458 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines_0.snap @@ -0,0 +1,15 @@ +[ + { + range: { + start: 0, + endExclusive: 21 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 3, + endColumn: 7 + }, + text: "line 1\nline 2\r\nline 3" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first_0.snap new file mode 100644 index 00000000000..b7b48f33be9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first_0.snap @@ -0,0 +1,73 @@ +[ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "Hello Mr. " + }, + { + range: { + start: 10, + endExclusive: 16 + }, + editorRange: { + startLineNumber: 1, + startColumn: 11, + endLineNumber: 1, + endColumn: 17 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } + } + }, + { + range: { + start: 16, + endExclusive: 17 + }, + editorRange: { + startLineNumber: 1, + startColumn: 17, + endLineNumber: 1, + endColumn: 18 + }, + text: " " + }, + { + range: { + start: 17, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 18, + endLineNumber: 1, + endColumn: 29 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 28, + endExclusive: 35 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 36 + }, + text: " thanks" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_0.snap new file mode 100644 index 00000000000..85afe6b0ae1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_0.snap @@ -0,0 +1,60 @@ +[ + { + range: { + start: 0, + endExclusive: 6 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 7 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } + } + }, + { + range: { + start: 6, + endExclusive: 17 + }, + editorRange: { + startLineNumber: 1, + startColumn: 7, + endLineNumber: 1, + endColumn: 18 + }, + text: " Please do " + }, + { + range: { + start: 17, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 18, + endLineNumber: 1, + endColumn: 29 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 28, + endExclusive: 35 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 36 + }, + text: " thanks" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command_0.snap new file mode 100644 index 00000000000..e1389633f29 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command_0.snap @@ -0,0 +1,15 @@ +[ + { + range: { + start: 0, + endExclusive: 13 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 14 + }, + text: "/explain this" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables_0.snap new file mode 100644 index 00000000000..d3fa8a51d2d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables_0.snap @@ -0,0 +1,15 @@ +[ + { + range: { + start: 0, + endExclusive: 26 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 27 + }, + text: "What does @selection mean?" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands_0.snap new file mode 100644 index 00000000000..3e1f3c0147e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands_0.snap @@ -0,0 +1,28 @@ +[ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + slashCommand: { command: "fix" } + }, + { + range: { + start: 4, + endExclusive: 9 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 10 + }, + text: " /fix" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_0.snap new file mode 100644 index 00000000000..d032da60253 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_0.snap @@ -0,0 +1,15 @@ +[ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + text: "test" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_0.snap new file mode 100644 index 00000000000..a2dadb07783 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_0.snap @@ -0,0 +1,28 @@ +[ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + slashCommand: { command: "fix" } + }, + { + range: { + start: 4, + endExclusive: 9 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 10 + }, + text: " this" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables_0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables_0.snap new file mode 100644 index 00000000000..75fe064e9aa --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables_0.snap @@ -0,0 +1,42 @@ +[ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "What does " + }, + { + range: { + start: 10, + endExclusive: 20 + }, + editorRange: { + startLineNumber: 1, + startColumn: 11, + endLineNumber: 1, + endColumn: 21 + }, + variableName: "selection", + variableArg: "" + }, + { + range: { + start: 20, + endExclusive: 26 + }, + editorRange: { + startLineNumber: 1, + startColumn: 21, + endLineNumber: 1, + endColumn: 27 + }, + text: " mean?" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts new file mode 100644 index 00000000000..7d207a080e5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { mockObject } from 'vs/base/test/common/mock'; +import { assertSnapshot } from 'vs/base/test/common/snapshot'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +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, 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 { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; + +suite('ChatRequestParser', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let parser: ChatRequestParser; + + setup(async () => { + instantiationService = testDisposables.add(new TestInstantiationService()); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + }); + + test('plain text', async () => { + parser = instantiationService.createInstance(ChatRequestParser); + const result = await parser.parseChatRequest('1', 'test'); + await assertSnapshot(result); + }); + + test('_plain text with newlines', async () => { + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'line 1\nline 2\r\nline 3'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('slash command', async () => { + const chatService = mockObject()({}); + chatService.getSlashCommands.returns(Promise.resolve([{ command: 'fix' }])); + instantiationService.stub(IChatService, chatService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = '/fix this'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('invalid slash command', async () => { + const chatService = mockObject()({}); + chatService.getSlashCommands.returns(Promise.resolve([{ command: 'fix' }])); + instantiationService.stub(IChatService, chatService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = '/explain this'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('multiple slash commands', async () => { + const chatService = mockObject()({}); + chatService.getSlashCommands.returns(Promise.resolve([{ command: 'fix' }])); + instantiationService.stub(IChatService, chatService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = '/fix /fix'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('variables', async () => { + const variablesService = mockObject()({}); + variablesService.hasVariable.returns(true); + instantiationService.stub(IChatVariablesService, variablesService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'What does @selection mean?'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('invalid variables', async () => { + const variablesService = mockObject()({}); + variablesService.hasVariable.returns(false); + instantiationService.stub(IChatVariablesService, variablesService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'What does @selection mean?'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('agents', async () => { + const agentsService = mockObject()({}); + agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); + instantiationService.stub(IChatAgentService, agentsService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const result = await parser.parseChatRequest('1', '@agent Please do /subCommand thanks'); + await assertSnapshot(result); + }); + + test('agent not first', async () => { + const agentsService = mockObject()({}); + agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); + instantiationService.stub(IChatAgentService, agentsService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const result = await parser.parseChatRequest('1', 'Hello Mr. @agent /subCommand thanks'); + await assertSnapshot(result); + }); + + test('_agents and variables and multiline', async () => { + const agentsService = mockObject()({}); + agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); + instantiationService.stub(IChatAgentService, agentsService as any); + + const variablesService = mockObject()({}); + variablesService.hasVariable.returns(true); + instantiationService.stub(IChatVariablesService, variablesService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const result = await parser.parseChatRequest('1', '@agent Please \ndo /subCommand with @selection\nand @debugConsole'); + await assertSnapshot(result); + }); +}); + diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 7c98a5622c1..fbdb839cd6b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -16,7 +16,7 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; +import { STARTING_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { Selection } from 'vs/editor/common/core/selection'; import { Emitter, Event } from 'vs/base/common/event'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -72,6 +72,8 @@ export class CommentNode extends Disposable { private _commentEditor: SimpleCommentEditor | null = null; private _commentEditorDisposables: IDisposable[] = []; private _commentEditorModel: ITextModel | null = null; + private _editorHeight = STARTING_EDITOR_HEIGHT; + private _isPendingLabel!: HTMLElement; private _timestamp: HTMLElement | undefined; private _timestampWidget: TimestampWidget | undefined; @@ -367,7 +369,8 @@ export class CommentNode extends Disposable { async submitComment(): Promise { if (this._commentEditor && this._commentFormActions) { - this._commentFormActions.triggerDefaultAction(); + await this._commentFormActions.triggerDefaultAction(); + this.pendingEdit = undefined; } } @@ -479,11 +482,11 @@ export class CommentNode extends Disposable { this._commentEditor.setModel(this._commentEditorModel); this._commentEditor.setValue(this.pendingEdit ?? this.commentBodyValue); this.pendingEdit = undefined; - this._commentEditor.layout({ width: container.clientWidth - 14, height: 90 }); + this._commentEditor.layout({ width: container.clientWidth - 14, height: this._editorHeight }); this._commentEditor.focus(); dom.scheduleAtNextAnimationFrame(() => { - this._commentEditor!.layout({ width: container.clientWidth - 14, height: 90 }); + this._commentEditor!.layout({ width: container.clientWidth - 14, height: this._editorHeight }); this._commentEditor!.focus(); }); @@ -518,10 +521,30 @@ export class CommentNode extends Disposable { } })); + this.calculateEditorHeight(); + + this._register((this._commentEditorModel.onDidChangeContent(() => { + if (this._commentEditor && this.calculateEditorHeight()) { + this._commentEditor.layout({ height: this._editorHeight, width: this._commentEditor.getLayoutInfo().width }); + this._commentEditor.render(true); + } + }))); + this._register(this._commentEditor); this._register(this._commentEditorModel); } + private calculateEditorHeight(): boolean { + if (this._commentEditor) { + const newEditorHeight = calculateEditorHeight(this._commentEditor, this._editorHeight); + if (newEditorHeight !== this._editorHeight) { + this._editorHeight = newEditorHeight; + return true; + } + } + return false; + } + getPendingEdit(): string | undefined { const model = this._commentEditor?.getModel(); if (model && model.getValueLength() > 0) { @@ -550,7 +573,7 @@ export class CommentNode extends Disposable { } layout() { - this._commentEditor?.layout(); + this._commentEditor?.layout({ width: this._commentEditor.getLayoutInfo().width, height: this._editorHeight }); const scrollWidth = this._body.scrollWidth; const width = dom.getContentWidth(this._body); const scrollHeight = this._body.scrollHeight; diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 87be57b8f00..e05c8f02faa 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -28,7 +28,7 @@ import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentSe import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { SimpleCommentEditor } from './simpleCommentEditor'; +import { STARTING_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from './simpleCommentEditor'; const COMMENT_SCHEME = 'comment'; let INMEM_MODEL_ID = 0; @@ -45,6 +45,7 @@ export class CommentReply extends Disposable { private _commentFormActions!: CommentFormActions; private _commentEditorActions!: CommentFormActions; private _reviewThreadReplyButton!: HTMLElement; + private _editorHeight = STARTING_EDITOR_HEIGHT; constructor( readonly owner: string, @@ -86,10 +87,15 @@ export class CommentReply extends Disposable { const model = this.modelService.createModel(this._pendingComment || '', this.languageService.createByFilepathOrFirstLine(resource), resource, false); this._register(model); this.commentEditor.setModel(model); + this.calculateEditorHeight(); - this._register((this.commentEditor.getModel()!.onDidChangeContent(() => { + this._register((model.onDidChangeContent(() => { this.setCommentEditorDecorations(); this.commentEditorIsEmpty?.set(!this.commentEditor.getValue()); + if (this.calculateEditorHeight()) { + this.commentEditor.layout({ height: this._editorHeight, width: this.commentEditor.getLayoutInfo().width }); + this.commentEditor.render(true); + } }))); this.createTextModelListener(this.commentEditor, this.form); @@ -110,6 +116,15 @@ export class CommentReply extends Disposable { this.createCommentWidgetEditorActions(this._editorActions, model); } + private calculateEditorHeight(): boolean { + const newEditorHeight = calculateEditorHeight(this.commentEditor, this._editorHeight); + if (newEditorHeight !== this._editorHeight) { + this._editorHeight = newEditorHeight; + return true; + } + return false; + } + public updateCommentThread(commentThread: languages.CommentThread) { const isReplying = this.commentEditor.hasTextFocus(); @@ -137,7 +152,7 @@ export class CommentReply extends Disposable { } public layout(widthInPixel: number) { - this.commentEditor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); + this.commentEditor.layout({ height: this._editorHeight, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); } public focusIfNeeded() { @@ -174,7 +189,8 @@ export class CommentReply extends Disposable { } async submitComment(): Promise { - return this._commentFormActions?.triggerDefaultAction(); + await this._commentFormActions?.triggerDefaultAction(); + this._pendingComment = undefined; } setCommentEditorDecorations() { diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 87c845189d8..1cf8bdb2c2b 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -382,7 +382,6 @@ } .review-widget .body .edit-textarea { - height: 90px; margin: 5px 0 10px 0; margin-right: 12px; } diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index 0ff28e47388..2e2f4a74d88 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { EditorAction, EditorContributionInstantiation, EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -25,9 +25,11 @@ import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/comment import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; export const ctxCommentEditorFocused = new RawContextKey('commentEditorFocused', false); - +export const STARTING_EDITOR_HEIGHT = 5 * 18; +export const MAX_EDITOR_HEIGHT = 25 * 18; export class SimpleCommentEditor extends CodeEditorWidget { private _parentThread: ICommentThreadWidget; @@ -109,3 +111,16 @@ export class SimpleCommentEditor extends CodeEditorWidget { }; } } + +export function calculateEditorHeight(editor: ICodeEditor, currentHeight: number): number { + const layoutInfo = editor.getLayoutInfo(); + const contentHeight = editor.getContentHeight(); + const lineHeight = editor.getOption(EditorOption.lineHeight); + if ((contentHeight > layoutInfo.height) || + (contentHeight < layoutInfo.height && currentHeight > STARTING_EDITOR_HEIGHT)) { + const linesToAdd = Math.ceil((contentHeight - layoutInfo.height) / lineHeight); + const newEditorHeight = Math.min(MAX_EDITOR_HEIGHT, layoutInfo.height + (lineHeight * linesToAdd)); + return newEditorHeight; + } + return currentHeight; +} diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 987705921bd..60e8ef89a10 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -454,10 +454,10 @@ configurationRegistry.registerConfiguration({ enum: ['floating', 'docked', 'commandCenter', 'hidden'], markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, `commandCenter` (requires `{0}`), or `hidden`.", '#window.commandCenter#', '#window.titleBarStyle#'), default: 'floating', - enumDescriptions: [ + markdownEnumDescriptions: [ nls.localize('debugToolBar.floating', "Show debug toolbar in all views."), nls.localize('debugToolBar.docked', "Show debug toolbar only in debug views."), - nls.localize('debugToolBar.commandCenter', "Show debug toolbar in the command center."), + nls.localize('debugToolBar.commandCenter', "`(Experimental)` Show debug toolbar in the command center."), nls.localize('debugToolBar.hidden', "Do not show debug toolbar."), ] }, diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index f5d418630f5..b899155fe46 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -59,7 +59,7 @@ export class EmptyView extends ViewPane { onDrop: e => { container.style.backgroundColor = ''; const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: !isWeb || isTemporaryWorkspace(this.contextService.getWorkspace()) }); - dropHandler.handleDrop(e, () => undefined, () => undefined); + dropHandler.handleDrop(e); }, onDragEnter: () => { const color = this.themeService.getColorTheme().getColor(listDropBackground); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 3304ca86197..593b813f336 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -708,7 +708,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop group, () => group.focus(), index); + this.dropHandler.handleDrop(originalEvent, () => group, () => group.focus(), { index }); } } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index b792b83f8ea..717d6cf8f59 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -483,9 +483,10 @@ export class ApplyPreviewEdits extends AbstractInlineChatAction { constructor() { super({ id: ACTION_ACCEPT_CHANGES, - title: localize('apply1', 'Accept Changes'), + title: { value: localize('apply1', 'Accept Changes'), original: 'Accept Changes' }, shortTitle: localize('apply2', 'Accept'), icon: Codicon.check, + f1: true, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ContextKeyExpr.or(CTX_INLINE_CHAT_DOCUMENT_CHANGED.toNegated(), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview))), keybinding: [{ weight: KeybindingWeight.EditorContrib + 10, @@ -493,7 +494,7 @@ export class ApplyPreviewEdits extends AbstractInlineChatAction { }, { primary: KeyCode.Escape, weight: KeybindingWeight.EditorContrib, - when: CTX_INLINE_CHAT_USER_DID_EDIT, + when: CTX_INLINE_CHAT_USER_DID_EDIT }], menu: { when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages), diff --git a/src/vs/workbench/contrib/localization/common/localizationsActions.ts b/src/vs/workbench/contrib/localization/common/localizationsActions.ts index 4e4f83ea12a..ac1e23a2411 100644 --- a/src/vs/workbench/contrib/localization/common/localizationsActions.ts +++ b/src/vs/workbench/contrib/localization/common/localizationsActions.ts @@ -65,9 +65,11 @@ export class ConfigureDisplayLanguageAction extends Action2 { }); disposables.add(qp.onDidAccept(async () => { - const selectedLanguage = qp.activeItems[0]; - qp.hide(); - await localeService.setLocale(selectedLanguage); + const selectedLanguage = qp.activeItems[0] as ILanguagePackItem | undefined; + if (selectedLanguage) { + qp.hide(); + await localeService.setLocale(selectedLanguage); + } })); disposables.add(qp.onDidTriggerItemButton(async e => { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 0b147c7f7c0..f6aee10d65c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -386,10 +386,26 @@ export abstract class BaseCellViewModel extends Disposable { private _removeCellDecoration(decorationId: string) { const options = this._resolvedCellDecorations.get(decorationId); + this._resolvedCellDecorations.delete(decorationId); if (options) { + for (const existingOptions of this._resolvedCellDecorations.values()) { + // don't remove decorations that are applied from other entries + if (options.className === existingOptions.className) { + options.className = undefined; + } + if (options.outputClassName === existingOptions.outputClassName) { + options.outputClassName = undefined; + } + if (options.gutterClassName === existingOptions.gutterClassName) { + options.gutterClassName = undefined; + } + if (options.topClassName === existingOptions.topClassName) { + options.topClassName = undefined; + } + } + this._cellDecorationsChanged.fire({ added: [], removed: [options] }); - this._resolvedCellDecorations.delete(decorationId); } } diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts new file mode 100644 index 00000000000..17fbe22abaa --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { Event } from 'vs/base/common/event'; + +suite('CellDecorations', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('Add and remove a cell decoration', async function () { + await withTestNotebook( + [ + ['# header a', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, viewModel) => { + const cell = viewModel.cellAt(0); + assert.ok(cell); + + let added = false; + Event.once(cell.onCellDecorationsChanged)(e => added = !!e.added.find(decoration => decoration.className === 'style1')); + + const decorationIds = cell.deltaCellDecorations([], [{ className: 'style1' }]); + assert.ok(cell.getCellDecorations().find(dec => dec.className === 'style1')); + + let removed = false; + Event.once(cell.onCellDecorationsChanged)(e => removed = !!e.removed.find(decoration => decoration.className === 'style1')); + cell.deltaCellDecorations(decorationIds, []); + + assert.ok(!cell.getCellDecorations().find(dec => dec.className === 'style1')); + assert.ok(added); + assert.ok(removed); + }); + }); + + test('Removing one cell decoration should not remove all', async function () { + await withTestNotebook( + [ + ['# header a', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, viewModel) => { + const cell = viewModel.cellAt(0); + assert.ok(cell); + + const decorationIds = cell.deltaCellDecorations([], [{ className: 'style1', outputClassName: 'style1' }]); + cell.deltaCellDecorations([], [{ className: 'style1' }]); + + let styleRemoved = false; + let outputStyleRemoved = false; + Event.once(cell.onCellDecorationsChanged)(e => { + styleRemoved = !!e.removed.find(decoration => decoration.className === 'style1'); + outputStyleRemoved = !!e.removed.find(decoration => decoration.outputClassName === 'style1'); + }); + // remove the first style added, which should only remove the output class + cell.deltaCellDecorations(decorationIds, []); + + assert.ok(!cell.getCellDecorations().find(dec => dec.outputClassName === 'style1')); + assert.ok(cell.getCellDecorations().find(dec => dec.className === 'style1')); + assert.ok(!styleRemoved); + assert.ok(outputStyleRemoved); + }); + }); +}); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index ad2a9b4a633..5a65487a491 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -282,7 +282,7 @@ export abstract class AbstractListSettingWidget extend this.addTooltipsToRow(rowElementGroup, item); if (item.selected && listFocused) { - this.listDisposables.add(disposableTimeout(() => rowElement.focus())); + disposableTimeout(() => rowElement.focus(), undefined, this.listDisposables); } this.listDisposables.add(DOM.addDisposableListener(rowElement, 'click', (e) => { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 47801632b7d..34016a59465 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2078,7 +2078,7 @@ class SCMInputWidget { this.disposables.add(this.inputEditor.onDidChangeCursorPosition(({ position }) => { const viewModel = this.inputEditor._getViewModel()!; const lastLineNumber = viewModel.getLineCount(); - const lastLineCol = viewModel.getLineContent(lastLineNumber).length + 1; + const lastLineCol = viewModel.getLineLength(lastLineNumber) + 1; const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); firstLineKey.set(viewPosition.lineNumber === 1 && viewPosition.column === 1); lastLineKey.set(viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 797498b8dca..e6ae569ad9d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -10,7 +10,7 @@ import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { AutoOpenBarrier, Promises, timeout } from 'vs/base/common/async'; +import { AutoOpenBarrier, Promises, disposableTimeout, timeout } from 'vs/base/common/async'; import { Codicon, getAllCodicons } from 'vs/base/common/codicons'; import { debounce } from 'vs/base/common/decorators'; import { ErrorNoTelemetry, onUnexpectedError } from 'vs/base/common/errors'; @@ -759,7 +759,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._lineDataEventAddon = lineDataEventAddon; // Delay the creation of the bell listener to avoid showing the bell when the terminal // starts up or reconnects - setTimeout(() => { + disposableTimeout(() => { this._register(xterm.raw.onBell(() => { if (this._configHelper.config.enableBell) { this.statusList.add({ @@ -771,7 +771,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._audioCueService.playSound(AudioCue.terminalBell.sound.getSound()); } })); - }, 1000); + }, 1000, this._store); this._register(xterm.raw.onSelectionChange(async () => this._onSelectionChange())); this._register(xterm.raw.buffer.onBufferChange(() => this._refreshAltBufferContextKey())); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index 3d8e715653b..897d9184586 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -70,7 +70,9 @@ export class TerminalStatusList extends Disposable implements ITerminalStatusLis let result: ITerminalStatus | undefined; for (const s of this._statuses.values()) { if (!result || s.severity >= result.severity) { - result = s; + if (s.icon || !result?.icon) { + result = s; + } } } return result; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 4ec363011fe..c04432f8b2f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -81,6 +81,7 @@ export class TerminalTabList extends WorkbenchList { @ILifecycleService lifecycleService: ILifecycleService, @IHoverService private readonly _hoverService: IHoverService, ) { + const dnd = instantiationService.createInstance(TerminalTabsDragAndDrop); super('TerminalTabsList', container, { getHeight: () => TerminalTabsListSizes.TabHeight, @@ -98,7 +99,7 @@ export class TerminalTabList extends WorkbenchList { smoothScrolling: _configurationService.getValue('workbench.list.smoothScrolling'), multipleSelectionSupport: true, paddingBottom: TerminalTabsListSizes.TabHeight, - dnd: instantiationService.createInstance(TerminalTabsDragAndDrop), + dnd, openOnSingleClick: true }, contextKeyService, @@ -106,6 +107,7 @@ export class TerminalTabList extends WorkbenchList { _configurationService, instantiationService, ); + this.disposables.add(dnd); const instanceDisposables: IDisposable[] = [ this._terminalGroupService.onDidChangeInstances(() => this.refresh()), @@ -559,14 +561,16 @@ class TerminalTabsAccessibilityProvider implements IListAccessibilityProvider { +class TerminalTabsDragAndDrop extends Disposable implements IListDragAndDrop { private _autoFocusInstance: ITerminalInstance | undefined; private _autoFocusDisposable: IDisposable = Disposable.None; private _primaryBackend: ITerminalBackend | undefined; + constructor( @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, ) { + super(); this._primaryBackend = this._terminalService.getPrimaryBackend(); } @@ -624,7 +628,7 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { this._autoFocusDisposable = disposableTimeout(() => { this._terminalService.setActiveInstance(targetInstance); this._autoFocusInstance = undefined; - }, 500); + }, 500, this._store); } return { diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts index af6a7c3fbe1..a7ef8dfbe66 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts @@ -92,14 +92,19 @@ suite('Workbench - TerminalStatusList', () => { }); test('onDidChangePrimaryStatus', async () => { - const result = await new Promise(r => { - store.add(list.onDidRemoveStatus(r)); + const result = await new Promise(r => { + store.add(list.onDidChangePrimaryStatus(r)); list.add({ id: 'test', severity: Severity.Info }); - list.remove('test'); }); deepStrictEqual(result, { id: 'test', severity: Severity.Info }); }); + test('primary is not updated to status without an icon', async () => { + list.add({ id: 'test', severity: Severity.Info, icon: Codicon.check }); + list.add({ id: 'warning', severity: Severity.Warning }); + deepStrictEqual(list.primary, { id: 'test', severity: Severity.Info, icon: Codicon.check }); + }); + test('add', () => { statusesEqual(list, []); list.add({ id: 'info', severity: Severity.Info }); diff --git a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts index 97d110f9b94..cbbb9b4309d 100644 --- a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts @@ -1383,6 +1383,7 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { } }, Math.max(500, this.stats.maxLatency * 3 / 2), + this._store ); } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 607712a3bcb..5b658089901 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -820,7 +820,21 @@ export class TimelinePane extends ViewPane { this.setLoadingUriMessage(); } else { this.updateFilename(this.labelService.getUriBasenameLabel(this.uri)); - this.message = localize('timeline.noTimelineInfo', "No timeline information was provided."); + const scmProviderCount = this.contextKeyService.getContextKeyValue('scm.providerCount'); + if (this.timelineService.getSources().filter(({ id }) => !this.excludedSources.has(id)).length === 0) { + this.message = localize('timeline.noTimelineSourcesEnabled', "All timeline sources have been filtered out."); + } else { + if (this.configurationService.getValue('workbench.localHistory.enabled') && !this.excludedSources.has('timeline.localHistory')) { + this.message = localize('timeline.noLocalHistoryYet', "Local History will track recent changes as you save them unless the file has been excluded or is too large."); + } else if (this.excludedSources.size > 0) { + this.message = localize('timeline.noTimelineInfoFromEnabledSources', "No filtered timeline information was provided."); + } else { + this.message = localize('timeline.noTimelineInfo', "No timeline information was provided."); + } + } + if (!scmProviderCount || scmProviderCount === 0) { + this.message += ' ' + localize('timeline.noSCM', "Source Control has not been configured."); + } } } else { this.updateFilename(this.labelService.getUriBasenameLabel(this.uri)); diff --git a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css index aa12a274620..b17aa5fb4d7 100644 --- a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css +++ b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css @@ -62,24 +62,63 @@ padding: 0 4px; } -.profile-type-widget { +.profile-edit-widget { + padding: 4px 6px 0px 11px; +} + +.profile-edit-widget > .profile-icon-container { + display: flex; + margin-bottom: 8px; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon { + cursor: pointer; + padding: 3px; + border-radius: 5px; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon.codicon{ + font-size: 18px; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon:hover { + outline: 1px dashed var(--vscode-toolbar-hoverOutline); + outline-offset: -1px; + background-color: var(--vscode-toolbar-hoverBackground); +} + +.profile-edit-widget > .profile-type-container { display: flex; - margin: 0px 6px 8px 11px; align-items: center; justify-content: space-between; font-size: 12px; + margin-bottom: 8px; } -.profile-type-widget>.profile-type-select-container { +.profile-edit-widget > .profile-icon-container > .profile-icon-label, +.profile-edit-widget > .profile-type-container > .profile-type-create-label { + width: 90px; + display: inline-flex; + align-items: center; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon-id { + display: inline-flex; + align-items: center; + margin-left: 5px; + opacity: .8; + font-size: 0.9em; +} + +.profile-edit-widget > .profile-type-container > .profile-type-select-container { overflow: hidden; - padding-left: 10px; flex: 1; display: flex; align-items: center; justify-content: center; } -.profile-type-widget>.profile-type-select-container>.monaco-select-box { +.profile-edit-widget > .profile-type-container > .profile-type-select-container > .monaco-select-box { cursor: pointer; line-height: 17px; padding: 2px 23px 2px 8px; diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index 9dc107dd7fb..5cef46ce00b 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -40,7 +40,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { defaultButtonStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { generateUuid } from 'vs/base/common/uuid'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorsOrder } from 'vs/workbench/common/editor'; @@ -71,10 +71,19 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConstants'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { DEFAULT_ICON, ICONS } from 'vs/workbench/services/userDataProfile/common/userDataProfileIcons'; +import { WorkbenchIconSelectBox } from 'vs/workbench/browser/iconSelectBox'; +import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; interface IUserDataProfileTemplate { readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly settings?: string; readonly keybindings?: string; readonly tasks?: string; @@ -89,6 +98,7 @@ function isUserDataProfileTemplate(thing: unknown): thing is IUserDataProfileTem return !!(candidate && typeof candidate === 'object' && (candidate.name && typeof candidate.name === 'string') && (isUndefined(candidate.shortName) || typeof candidate.shortName === 'string') + && (isUndefined(candidate.icon) || typeof candidate.icon === 'string') && (isUndefined(candidate.settings) || typeof candidate.settings === 'string') && (isUndefined(candidate.globalState) || typeof candidate.globalState === 'string') && (isUndefined(candidate.extensions) || typeof candidate.extensions === 'string')); @@ -132,6 +142,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IContextViewService private readonly contextViewService: IContextViewService, + @IHoverService private readonly hoverService: IHoverService, @ILogService private readonly logService: ILogService, ) { super(); @@ -322,7 +333,14 @@ export class UserDataProfileImportExportService extends Disposable implements IU disposables.add(quickPick.onDidChangeValue(validate)); - let result: { name: string; items: ReadonlyArray } | undefined; + let icon = DEFAULT_ICON; + if (profile?.icon) { + icon = ThemeIcon.fromId(profile.icon); + } + if (isUserDataProfileTemplate(source) && source.icon) { + icon = ThemeIcon.fromId(source.icon); + } + let result: { name: string; items: ReadonlyArray; icon?: string | null } | undefined; disposables.add(Event.any(quickPick.onDidCustom, quickPick.onDidAccept)(() => { const name = quickPick.value.trim(); if (!name) { @@ -332,15 +350,74 @@ export class UserDataProfileImportExportService extends Disposable implements IU if (quickPick.validationMessage) { return; } - result = { name, items: quickPick.selectedItems }; + result = { name, items: quickPick.selectedItems, icon: icon.id === DEFAULT_ICON.id ? null : icon.id }; quickPick.hide(); quickPick.severity = Severity.Ignore; quickPick.validationMessage = undefined; })); + const domNode = DOM.$('.profile-edit-widget'); + + const profileIconContainer = DOM.$('.profile-icon-container'); + DOM.append(profileIconContainer, DOM.$('.profile-icon-label', undefined, localize('icon', "Icon:"))); + const profileIconElement = DOM.append(profileIconContainer, DOM.$(`.profile-icon${ThemeIcon.asCSSSelector(icon)}`)); + profileIconElement.tabIndex = 0; + profileIconElement.role = 'button'; + profileIconElement.ariaLabel = localize('select icon', "Icon: {0}", icon.id); + const iconSelectBox = disposables.add(this.instantiationService.createInstance(WorkbenchIconSelectBox, { icons: ICONS, inputBoxStyles: defaultInputBoxStyles })); + const dimension = new DOM.Dimension(496, 260); + iconSelectBox.layout(dimension); + let hoverWidget: IHoverWidget | undefined; + + const updateIcon = (updated: ThemeIcon | undefined) => { + icon = updated ?? DEFAULT_ICON; + profileIconElement.className = `profile-icon ${ThemeIcon.asClassName(icon)}`; + }; + disposables.add(iconSelectBox.onDidSelect(selectedIcon => { + if (icon.id !== selectedIcon.id) { + updateIcon(selectedIcon); + } + hoverWidget?.dispose(); + profileIconElement.focus(); + })); + const showIconSelectBox = () => { + iconSelectBox.clearInput(); + hoverWidget = this.hoverService.showHover({ + content: iconSelectBox.domNode, + target: profileIconElement, + hoverPosition: HoverPosition.BELOW, + showPointer: true, + hideOnHover: false, + }, true); + if (hoverWidget) { + iconSelectBox.layout(dimension); + disposables.add(hoverWidget); + } + iconSelectBox.focus(); + }; + disposables.add(DOM.addDisposableListener(profileIconElement, DOM.EventType.CLICK, (e: MouseEvent) => { + DOM.EventHelper.stop(e, true); + showIconSelectBox(); + })); + disposables.add(DOM.addDisposableListener(profileIconElement, DOM.EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + DOM.EventHelper.stop(event, true); + showIconSelectBox(); + } + })); + disposables.add(DOM.addDisposableListener(iconSelectBox.domNode, DOM.EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Escape)) { + DOM.EventHelper.stop(event, true); + hoverWidget?.dispose(); + profileIconElement.focus(); + } + })); + if (!profile && !isUserDataProfileTemplate(source)) { - const domNode = DOM.$('.profile-type-widget'); - DOM.append(domNode, DOM.$('.profile-type-create-label', undefined, localize('create from', "Copy from:"))); + const profileTypeContainer = DOM.append(domNode, DOM.$('.profile-type-container')); + DOM.append(profileTypeContainer, DOM.$('.profile-type-create-label', undefined, localize('create from', "Copy from:"))); const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; const profileOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] = []; profileOptions.push({ text: localize('empty profile', "None") }); @@ -369,9 +446,17 @@ export class UserDataProfileImportExportService extends Disposable implements IU }; const initialIndex = findOptionIndex(); - const selectBox = disposables.add(this.instantiationService.createInstance(SelectBox, profileOptions, initialIndex, this.contextViewService, defaultSelectBoxStyles, { useCustomDrawn: true })); - selectBox.render(DOM.append(domNode, DOM.$('.profile-type-select-container'))); - quickPick.widget = domNode; + const selectBox = disposables.add(this.instantiationService.createInstance(SelectBox, + profileOptions, + initialIndex, + this.contextViewService, + defaultSelectBoxStyles, + { + useCustomDrawn: true, + ariaLabel: localize('copy profile from', "Copy profile from"), + } + )); + selectBox.render(DOM.append(profileTypeContainer, DOM.$('.profile-type-select-container'))); if (profileOptions[initialIndex].source) { quickPick.value = this.generateProfileName(profileOptions[initialIndex].text); @@ -382,6 +467,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU for (const resource of resources) { resource.picked = option.source && !(option.source instanceof URI) ? !option.source?.useDefaultFlags?.[resource.id] : true; } + updateIcon(!(option.source instanceof URI) && option.source?.icon ? ThemeIcon.fromId(option.source.icon) : undefined); update(); }; @@ -392,6 +478,9 @@ export class UserDataProfileImportExportService extends Disposable implements IU })); } + DOM.append(domNode, profileIconContainer); + + quickPick.widget = domNode; quickPick.show(); await new Promise((c, e) => { @@ -421,21 +510,21 @@ export class UserDataProfileImportExportService extends Disposable implements IU extensions: !result.items.includes(extensions) }; if (profile) { - await this.userDataProfileManagementService.updateProfile(profile, { name: result.name, useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); + await this.userDataProfileManagementService.updateProfile(profile, { name: result.name, icon: result.icon, useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); } else { if (source instanceof URI) { this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); - await this.importProfile(source, { mode: 'apply', name: result.name, useDefaultFlags }); + await this.importProfile(source, { mode: 'apply', name: result.name, useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfile(source)) { this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); - await this.createFromProfile(source, result.name, { useDefaultFlags }); + await this.createFromProfile(source, result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfileTemplate(source)) { source.name = result.name; this.telemetryService.publicLog2('userDataProfile.createFromExternalTemplate', createProfileTelemetryData); - await this.createAndSwitch(source, false, true, { useDefaultFlags }, localize('create profile', "Create Profile")); + await this.createAndSwitch(source, false, true, { useDefaultFlags, icon: result.icon ? result.icon : undefined }, localize('create profile', "Create Profile")); } else { this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); - await this.userDataProfileManagementService.createAndEnterProfile(result.name, { useDefaultFlags }); + await this.userDataProfileManagementService.createAndEnterProfile(result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); } } } catch (error) { @@ -600,6 +689,10 @@ export class UserDataProfileImportExportService extends Disposable implements IU profileTemplate.name = options.name; } + if (options?.icon) { + profileTemplate.icon = options.icon; + } + return profileTemplate; } @@ -1315,6 +1408,7 @@ class UserDataProfileExportState extends UserDataProfileImportExportState { location: profile.location, isDefault: profile.isDefault, shortName: profile.shortName, + icon: profile.icon, globalStorageHome: profile.globalStorageHome, settingsResource: profile.settingsResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }), keybindingsResource: profile.keybindingsResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }), diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 25347a80b98..95b19167137 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -80,6 +80,7 @@ export function toUserDataProfileUri(path: string, productService: IProductServi export interface IProfileImportOptions extends IUserDataProfileOptions { readonly name?: string; + readonly icon?: string; readonly mode?: 'preview' | 'apply' | 'both'; } diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts new file mode 100644 index 00000000000..5864da3bd62 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export const DEFAULT_ICON = Codicon.settingsGear; + +export const ICONS = [ + + /* Default */ + DEFAULT_ICON, + + /* hardware/devices */ + Codicon.vm, + Codicon.server, + Codicon.recordKeys, + Codicon.deviceMobile, + Codicon.watch, + + /* languages */ + Codicon.ruby, + Codicon.code, + + /* project types */ + Codicon.window, + Codicon.library, + Codicon.extensions, + Codicon.terminal, + Codicon.beaker, + Codicon.package, + Codicon.cloud, + Codicon.book, + Codicon.globe, + Codicon.database, + Codicon.notebook, + + /* misc */ + Codicon.gift, + Codicon.send, + Codicon.briefcase, + Codicon.megaphone, + Codicon.comment, + Codicon.telescope, + Codicon.creditCard, + Codicon.map, + Codicon.deviceCameraVideo, + Codicon.unmute, + Codicon.law, + Codicon.graphLine, + Codicon.heart, + Codicon.home, + Codicon.inbox, + Codicon.mortarBoard, + Codicon.rocket, + Codicon.magnet, + Codicon.lock, + Codicon.milestone, + Codicon.tag, + Codicon.pulse, + Codicon.radioTower, + Codicon.smiley, + Codicon.symbolEvent, + Codicon.squirrel, + Codicon.symbolColor, + Codicon.mail, + Codicon.key, + Codicon.pieChart, + Codicon.organization, + Codicon.preview, + Codicon.wand, + Codicon.starEmpty, + Codicon.lightbulb, + Codicon.symbolRuler, + Codicon.dashboard, + Codicon.calendar, + Codicon.shield, + Codicon.flame, + Codicon.compass, + Codicon.paintcan, + Codicon.archive, + Codicon.mic, + +]; diff --git a/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts index 55046c54561..1f9254fd590 100644 --- a/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts @@ -123,16 +123,13 @@ export abstract class ResourceWorkingCopy extends Disposable implements IResourc private readonly _onWillDispose = this._register(new Emitter()); readonly onWillDispose = this._onWillDispose.event; - private disposed = false; - isDisposed(): boolean { - return this.disposed; + return this._store.isDisposed; } override dispose(): void { // State - this.disposed = true; this.orphaned = false; // Event diff --git a/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts new file mode 100644 index 00000000000..23df936d03d --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts @@ -0,0 +1,794 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { EditorGroupModel, ISerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; +import { EditorExtensions, IEditorFactoryRegistry, IFileEditorInput, IEditorSerializer, EditorsOrder, GroupModelChangeKind } from 'vs/workbench/common/editor'; +import { URI } from 'vs/base/common/uri'; +import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { isEqual } from 'vs/base/common/resources'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { StickyEditorGroupModel, UnstickyEditorGroupModel } from 'vs/workbench/common/editor/filteredEditorGroupModel'; + +suite('FilteredEditorGroupModel', () => { + + let testInstService: TestInstantiationService | undefined; + + suiteTeardown(() => { + testInstService?.dispose(); + testInstService = undefined; + }); + + function inst(): IInstantiationService { + if (!testInstService) { + testInstService = new TestInstantiationService(); + } + const inst = testInstService; + inst.stub(IStorageService, disposables.add(new TestStorageService())); + inst.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + inst.stub(IWorkspaceContextService, new TestContextService()); + inst.stub(ITelemetryService, NullTelemetryService); + + const config = new TestConfigurationService(); + config.setUserConfiguration('workbench', { editor: { openPositioning: 'right', focusRecentEditorAfterClose: true } }); + inst.stub(IConfigurationService, config); + + return inst; + } + + function createEditorGroupModel(serialized?: ISerializedEditorGroupModel): EditorGroupModel { + const group = disposables.add(inst().createInstance(EditorGroupModel, serialized)); + + disposables.add(toDisposable(() => { + for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + group.closeEditor(editor); + } + })); + + return group; + } + + let index = 0; + class TestEditorInput extends EditorInput { + + readonly resource = undefined; + + constructor(public id: string) { + super(); + } + override get typeId() { return 'testEditorInputForGroups'; } + override async resolve(): Promise { return null!; } + + override matches(other: TestEditorInput): boolean { + return other && this.id === other.id && other instanceof TestEditorInput; + } + + setDirty(): void { + this._onDidChangeDirty.fire(); + } + + setLabel(): void { + this._onDidChangeLabel.fire(); + } + } + + class NonSerializableTestEditorInput extends EditorInput { + + readonly resource = undefined; + + constructor(public id: string) { + super(); + } + override get typeId() { return 'testEditorInputForGroups-nonSerializable'; } + override async resolve(): Promise { return null; } + + override matches(other: NonSerializableTestEditorInput): boolean { + return other && this.id === other.id && other instanceof NonSerializableTestEditorInput; + } + } + + class TestFileEditorInput extends EditorInput implements IFileEditorInput { + + readonly preferredResource = this.resource; + + constructor(public id: string, public resource: URI) { + super(); + } + override get typeId() { return 'testFileEditorInputForGroups'; } + override get editorId() { return this.id; } + override async resolve(): Promise { return null; } + setPreferredName(name: string): void { } + setPreferredDescription(description: string): void { } + setPreferredResource(resource: URI): void { } + async setEncoding(encoding: string) { } + getEncoding() { return undefined; } + setPreferredEncoding(encoding: string) { } + setForceOpenAsBinary(): void { } + setPreferredContents(contents: string): void { } + setLanguageId(languageId: string) { } + setPreferredLanguageId(languageId: string) { } + isResolved(): boolean { return false; } + + override matches(other: TestFileEditorInput): boolean { + if (super.matches(other)) { + return true; + } + + if (other instanceof TestFileEditorInput) { + return isEqual(other.resource, this.resource); + } + + return false; + } + } + + function input(id = String(index++), nonSerializable?: boolean, resource?: URI): EditorInput { + if (resource) { + return disposables.add(new TestFileEditorInput(id, resource)); + } + + return nonSerializable ? disposables.add(new NonSerializableTestEditorInput(id)) : disposables.add(new TestEditorInput(id)); + } + + function closeAllEditors(group: EditorGroupModel): void { + for (const editor of group.getEditors(EditorsOrder.SEQUENTIAL)) { + group.closeEditor(editor, undefined, false); + } + } + + interface ISerializedTestInput { + id: string; + } + + class TestEditorInputSerializer implements IEditorSerializer { + + static disableSerialize = false; + static disableDeserialize = false; + + canSerialize(editorInput: EditorInput): boolean { + return true; + } + + serialize(editorInput: EditorInput): string | undefined { + if (TestEditorInputSerializer.disableSerialize) { + return undefined; + } + + const testEditorInput = editorInput; + const testInput: ISerializedTestInput = { + id: testEditorInput.id + }; + + return JSON.stringify(testInput); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined { + if (TestEditorInputSerializer.disableDeserialize) { + return undefined; + } + + const testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); + + return disposables.add(new TestEditorInput(testInput.id)); + } + } + + const disposables = new DisposableStore(); + + setup(() => { + TestEditorInputSerializer.disableSerialize = false; + TestEditorInputSerializer.disableDeserialize = false; + + disposables.add(Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer('testEditorInputForGroups', TestEditorInputSerializer)); + }); + + teardown(() => { + disposables.clear(); + + index = 1; + }); + + test('Sticky/Unsticky count', async () => { + + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: true }); + + + assert.strictEqual(stickyFilteredEditorGroup.count, 2); + assert.strictEqual(unstickyFilteredEditorGroup.count, 0); + + model.unstick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.count, 1); + assert.strictEqual(unstickyFilteredEditorGroup.count, 1); + + model.unstick(input2); + + assert.strictEqual(stickyFilteredEditorGroup.count, 0); + assert.strictEqual(unstickyFilteredEditorGroup.count, 2); + }); + + test('Sticky/Unsticky stickyCount', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: true }); + + + assert.strictEqual(stickyFilteredEditorGroup.stickyCount, 2); + assert.strictEqual(unstickyFilteredEditorGroup.stickyCount, 0); + + model.unstick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.stickyCount, 1); + assert.strictEqual(unstickyFilteredEditorGroup.stickyCount, 0); + + model.unstick(input2); + + assert.strictEqual(stickyFilteredEditorGroup.stickyCount, 0); + assert.strictEqual(unstickyFilteredEditorGroup.stickyCount, 0); + }); + + test('Sticky/Unsticky isEmpty', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: false }); + model.openEditor(input2, { pinned: true, sticky: false }); + + + assert.strictEqual(stickyFilteredEditorGroup.count === 0, true); + assert.strictEqual(unstickyFilteredEditorGroup.count === 0, false); + + model.stick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.count === 0, false); + assert.strictEqual(unstickyFilteredEditorGroup.count === 0, false); + + model.stick(input2); + + assert.strictEqual(stickyFilteredEditorGroup.count === 0, false); + assert.strictEqual(unstickyFilteredEditorGroup.count === 0, true); + }); + + test('Sticky/Unsticky editors', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 2); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 0); + + model.unstick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 1); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 1); + + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL)[0], input2); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); + + model.unstick(input2); + + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 0); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 2); + }); + + test('Sticky/Unsticky activeEditor', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true, active: true }); + + assert.strictEqual(stickyFilteredEditorGroup.activeEditor, input1); + assert.strictEqual(unstickyFilteredEditorGroup.activeEditor, null); + + model.openEditor(input2, { pinned: true, sticky: false, active: true }); + + assert.strictEqual(stickyFilteredEditorGroup.activeEditor, null); + assert.strictEqual(unstickyFilteredEditorGroup.activeEditor, input2); + + model.closeEditor(input1); + + assert.strictEqual(stickyFilteredEditorGroup.activeEditor, null); + assert.strictEqual(unstickyFilteredEditorGroup.activeEditor, input2); + + model.closeEditor(input2); + + assert.strictEqual(stickyFilteredEditorGroup.activeEditor, null); + assert.strictEqual(unstickyFilteredEditorGroup.activeEditor, null); + }); + + test('Sticky/Unsticky previewEditor', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1); + + assert.strictEqual(stickyFilteredEditorGroup.previewEditor, null); + assert.strictEqual(unstickyFilteredEditorGroup.previewEditor, input1); + + model.openEditor(input2, { sticky: true }); + assert.strictEqual(stickyFilteredEditorGroup.previewEditor, null); + assert.strictEqual(unstickyFilteredEditorGroup.previewEditor, input1); + }); + + test('Sticky/Unsticky isSticky()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.isSticky(input1), true); + assert.strictEqual(stickyFilteredEditorGroup.isSticky(input2), true); + + model.unstick(input1); + model.closeEditor(input1); + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(unstickyFilteredEditorGroup.isSticky(input1), false); + assert.strictEqual(unstickyFilteredEditorGroup.isSticky(input2), false); + }); + + test('Sticky/Unsticky isPinned()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + const input3 = input(); + const input4 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: false }); + model.openEditor(input3, { pinned: false, sticky: true }); + model.openEditor(input4, { pinned: false, sticky: false }); + + assert.strictEqual(stickyFilteredEditorGroup.isPinned(input1), true); + assert.strictEqual(unstickyFilteredEditorGroup.isPinned(input2), true); + assert.strictEqual(stickyFilteredEditorGroup.isPinned(input3), true); + assert.strictEqual(unstickyFilteredEditorGroup.isPinned(input4), false); + }); + + test('Sticky/Unsticky isActive()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true, active: true }); + + assert.strictEqual(stickyFilteredEditorGroup.isActive(input1), true); + + model.openEditor(input2, { pinned: true, sticky: false, active: true }); + + assert.strictEqual(stickyFilteredEditorGroup.isActive(input1), false); + assert.strictEqual(unstickyFilteredEditorGroup.isActive(input2), true); + + model.unstick(input1); + + assert.strictEqual(unstickyFilteredEditorGroup.isActive(input1), false); + assert.strictEqual(unstickyFilteredEditorGroup.isActive(input2), true); + }); + + test('Sticky/Unsticky getEditors()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true, active: true }); + model.openEditor(input2, { pinned: true, sticky: true, active: true }); + + // all sticky editors + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 2); + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2); + + // no unsticky editors + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 0); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); + + // options: excludeSticky + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 0); + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: false }).length, 2); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 0); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: false }).length, 0); + + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0], input2); + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1], input1); + + model.unstick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 1); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); + + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0], input2); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); + + model.unstick(input2); + + // all unsticky editors + assert.strictEqual(stickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL).length, 0); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2); + + // order: MOST_RECENTLY_ACTIVE + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0], input2); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1], input1); + + // order: SEQUENTIAL + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL)[0], input2); + assert.strictEqual(unstickyFilteredEditorGroup.getEditors(EditorsOrder.SEQUENTIAL)[1], input1); + }); + + test('Sticky/Unsticky getEditorByIndex()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + const input3 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(0), input1); + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(1), input2); + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(2), undefined); + assert.strictEqual(unstickyFilteredEditorGroup.getEditorByIndex(0), undefined); + assert.strictEqual(unstickyFilteredEditorGroup.getEditorByIndex(1), undefined); + + model.openEditor(input3, { pinned: true, sticky: false }); + + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(0), input1); + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(1), input2); + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(2), undefined); + assert.strictEqual(unstickyFilteredEditorGroup.getEditorByIndex(0), input3); + assert.strictEqual(unstickyFilteredEditorGroup.getEditorByIndex(1), undefined); + + model.unstick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(0), input2); + assert.strictEqual(stickyFilteredEditorGroup.getEditorByIndex(1), undefined); + assert.strictEqual(unstickyFilteredEditorGroup.getEditorByIndex(0), input1); + assert.strictEqual(unstickyFilteredEditorGroup.getEditorByIndex(1), input3); + assert.strictEqual(unstickyFilteredEditorGroup.getEditorByIndex(2), undefined); + }); + + test('Sticky/Unsticky indexOf()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + const input3 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input1), 0); + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input2), 1); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input1), -1); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input2), -1); + + model.openEditor(input3, { pinned: true, sticky: false }); + + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input1), 0); + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input2), 1); + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input3), -1); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input1), -1); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input2), -1); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input3), 0); + + model.unstick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input1), -1); + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input2), 0); + assert.strictEqual(stickyFilteredEditorGroup.indexOf(input3), -1); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input1), 0); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input2), -1); + assert.strictEqual(unstickyFilteredEditorGroup.indexOf(input3), 1); + }); + + test('Sticky/Unsticky isFirst()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.isFirst(input1), true); + + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.isFirst(input1), true); + assert.strictEqual(stickyFilteredEditorGroup.isFirst(input2), false); + + model.unstick(input1); + + assert.strictEqual(unstickyFilteredEditorGroup.isFirst(input1), true); + assert.strictEqual(stickyFilteredEditorGroup.isFirst(input2), true); + + model.unstick(input2); + + assert.strictEqual(unstickyFilteredEditorGroup.isFirst(input1), false); + assert.strictEqual(unstickyFilteredEditorGroup.isFirst(input2), true); + + model.moveEditor(input2, 1); + + assert.strictEqual(unstickyFilteredEditorGroup.isFirst(input1), true); + assert.strictEqual(unstickyFilteredEditorGroup.isFirst(input2), false); + }); + + test('Sticky/Unsticky isLast()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.isLast(input1), true); + + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.isLast(input1), false); + assert.strictEqual(stickyFilteredEditorGroup.isLast(input2), true); + + model.unstick(input1); + + assert.strictEqual(unstickyFilteredEditorGroup.isLast(input1), true); + assert.strictEqual(stickyFilteredEditorGroup.isLast(input2), true); + + model.unstick(input2); + + assert.strictEqual(unstickyFilteredEditorGroup.isLast(input1), true); + assert.strictEqual(unstickyFilteredEditorGroup.isLast(input2), false); + + model.moveEditor(input2, 1); + + assert.strictEqual(unstickyFilteredEditorGroup.isLast(input1), false); + assert.strictEqual(unstickyFilteredEditorGroup.isLast(input2), true); + }); + + test('Sticky/Unsticky contains()', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + const input1 = input(); + const input2 = input(); + + model.openEditor(input1, { pinned: true, sticky: true }); + model.openEditor(input2, { pinned: true, sticky: true }); + + assert.strictEqual(stickyFilteredEditorGroup.contains(input1), true); + assert.strictEqual(stickyFilteredEditorGroup.contains(input2), true); + + assert.strictEqual(unstickyFilteredEditorGroup.contains(input1), false); + assert.strictEqual(unstickyFilteredEditorGroup.contains(input2), false); + + model.unstick(input1); + + assert.strictEqual(stickyFilteredEditorGroup.contains(input1), false); + assert.strictEqual(stickyFilteredEditorGroup.contains(input2), true); + + assert.strictEqual(unstickyFilteredEditorGroup.contains(input1), true); + assert.strictEqual(unstickyFilteredEditorGroup.contains(input2), false); + + model.unstick(input2); + + assert.strictEqual(stickyFilteredEditorGroup.contains(input1), false); + assert.strictEqual(stickyFilteredEditorGroup.contains(input2), false); + + assert.strictEqual(unstickyFilteredEditorGroup.contains(input1), true); + assert.strictEqual(unstickyFilteredEditorGroup.contains(input2), true); + }); + + test('Sticky/Unsticky group information', async () => { + const model = createEditorGroupModel(); + + const stickyFilteredEditorGroup = disposables.add(new StickyEditorGroupModel(model)); + const unstickyFilteredEditorGroup = disposables.add(new UnstickyEditorGroupModel(model)); + + // same id + assert.strictEqual(stickyFilteredEditorGroup.id, model.id); + assert.strictEqual(unstickyFilteredEditorGroup.id, model.id); + + // group locking same behaviour + assert.strictEqual(stickyFilteredEditorGroup.isLocked, model.isLocked); + assert.strictEqual(unstickyFilteredEditorGroup.isLocked, model.isLocked); + + model.lock(true); + + assert.strictEqual(stickyFilteredEditorGroup.isLocked, model.isLocked); + assert.strictEqual(unstickyFilteredEditorGroup.isLocked, model.isLocked); + + model.lock(false); + + assert.strictEqual(stickyFilteredEditorGroup.isLocked, model.isLocked); + assert.strictEqual(unstickyFilteredEditorGroup.isLocked, model.isLocked); + }); + + test('Multiple Editors - Editor Emits Dirty and Label Changed', function () { + const model1 = createEditorGroupModel(); + const model2 = createEditorGroupModel(); + + const stickyFilteredEditorGroup1 = disposables.add(new StickyEditorGroupModel(model1)); + const unstickyFilteredEditorGroup1 = disposables.add(new UnstickyEditorGroupModel(model1)); + const stickyFilteredEditorGroup2 = disposables.add(new StickyEditorGroupModel(model2)); + const unstickyFilteredEditorGroup2 = disposables.add(new UnstickyEditorGroupModel(model2)); + + const input1 = input(); + const input2 = input(); + + model1.openEditor(input1, { pinned: true, active: true }); + model2.openEditor(input2, { pinned: true, active: true, sticky: true }); + + // DIRTY + let dirty1CounterSticky = 0; + disposables.add(stickyFilteredEditorGroup1.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_DIRTY) { + dirty1CounterSticky++; + } + })); + + let dirty1CounterUnsticky = 0; + disposables.add(unstickyFilteredEditorGroup1.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_DIRTY) { + dirty1CounterUnsticky++; + } + })); + + let dirty2CounterSticky = 0; + disposables.add(stickyFilteredEditorGroup2.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_DIRTY) { + dirty2CounterSticky++; + } + })); + + let dirty2CounterUnsticky = 0; + disposables.add(unstickyFilteredEditorGroup2.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_DIRTY) { + dirty2CounterUnsticky++; + } + })); + + // LABEL + let label1ChangeCounterSticky = 0; + disposables.add(stickyFilteredEditorGroup1.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_LABEL) { + label1ChangeCounterSticky++; + } + })); + + let label1ChangeCounterUnsticky = 0; + disposables.add(unstickyFilteredEditorGroup1.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_LABEL) { + label1ChangeCounterUnsticky++; + } + })); + + let label2ChangeCounterSticky = 0; + disposables.add(stickyFilteredEditorGroup2.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_LABEL) { + label2ChangeCounterSticky++; + } + })); + + let label2ChangeCounterUnsticky = 0; + disposables.add(unstickyFilteredEditorGroup2.onDidModelChange((e) => { + if (e.kind === GroupModelChangeKind.EDITOR_LABEL) { + label2ChangeCounterUnsticky++; + } + })); + + (input1).setDirty(); + (input1).setLabel(); + + assert.strictEqual(dirty1CounterSticky, 0); + assert.strictEqual(dirty1CounterUnsticky, 1); + assert.strictEqual(label1ChangeCounterSticky, 0); + assert.strictEqual(label1ChangeCounterUnsticky, 1); + + (input2).setDirty(); + (input2).setLabel(); + + assert.strictEqual(dirty2CounterSticky, 1); + assert.strictEqual(dirty2CounterUnsticky, 0); + assert.strictEqual(label2ChangeCounterSticky, 1); + assert.strictEqual(label2ChangeCounterUnsticky, 0); + + closeAllEditors(model2); + + (input2).setDirty(); + (input2).setLabel(); + + assert.strictEqual(dirty2CounterSticky, 1); + assert.strictEqual(dirty2CounterUnsticky, 0); + assert.strictEqual(label2ChangeCounterSticky, 1); + assert.strictEqual(label2ChangeCounterUnsticky, 0); + assert.strictEqual(dirty1CounterSticky, 0); + assert.strictEqual(dirty1CounterUnsticky, 1); + assert.strictEqual(label1ChangeCounterSticky, 0); + assert.strictEqual(label1ChangeCounterUnsticky, 1); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); diff --git a/src/vscode-dts/vscode.proposed.resolvers.d.ts b/src/vscode-dts/vscode.proposed.resolvers.d.ts index a1093a1b98e..5e0093143d5 100644 --- a/src/vscode-dts/vscode.proposed.resolvers.d.ts +++ b/src/vscode-dts/vscode.proposed.resolvers.d.ts @@ -150,10 +150,129 @@ declare module 'vscode' { } /** - * Exec server used for nested resolvers. The type is currently not maintained - * in these types, and is a contract between extensions. + * An ExecServer allows spawning processes on a remote machine. An ExecServer is provided by resolvers. It can be + * acquired by `workspace.getRemoteExecServer` or from the context when in a resolver (`RemoteAuthorityResolverContext.execServer`). */ - export type ExecServer = unknown; + export interface ExecServer { + /** + * Spawns a given subprocess with the given command and arguments. + * @param command The command to execute. + * @param args The arguments to pass to the command. + * @param options Additional options for the spawned process. + * @returns A promise that gives access to the process' stdin, stdout and stderr streams, as well as the process' exit code. + */ + spawn(command: string, args: string[], options?: ExecServerSpawnOptions): Thenable; + + /** + * Spawns an connector that allows to start a remote server. It is assumed the command starts a Code CLI. Additional + * arguments will be passed to the connector. + * @param command The command to execute. It is assumed the command spawns a Code CLI executable. + * @param args The arguments to pass to the connector + * @param options Additional options for the spawned process. + * @returns A promise that gives access to the spawned {@link RemoteServerConnector}. It also provides a stream to which standard + * log messages are written. + */ + spawnRemoteServerConnector?(command: string, args: string[], options?: ExecServerSpawnOptions): Thenable; + + /** + * Downloads the CLI executable of the desired platform and quality and pipes it to the + * provided process' stdin. + * @param buildTarget The CLI build target to download. + * @param command The command to execute. The downloaded bits will be piped to the command's stdin. + * @param args The arguments to pass to the command. + * @param options Additional options for the spawned process. + * @returns A promise that resolves when the process exits with a {@link ProcessExit} object. + */ + downloadCliExecutable?(buildTarget: CliBuild, command: string, args: string[], options?: ExecServerSpawnOptions): Thenable; + + /** + * Gets the environment where the exec server is running. + * @returns A promise that resolves to an {@link ExecEnvironment} object. + */ + env(): Thenable; + + /** + * Access to the file system of the remote. + */ + readonly fs: RemoteFileSystem; + } + + export type ProcessEnv = Record; + + export interface ExecServerSpawnOptions { + readonly env?: ProcessEnv; + readonly cwd?: string; + } + + export interface SpawnedCommand { + readonly stdin: WriteStream; + readonly stdout: ReadStream; + readonly stderr: ReadStream; + readonly onExit: Thenable; + } + + export interface RemoteServerConnector { + readonly logs: ReadStream; + readonly onExit: Thenable; + /** + * Connect to a new code server, returning a stream that can be used to communicate with it. + * @param params The parameters for the code server. + * @returns A promise that resolves to a {@link ManagedMessagePassing} object that can be used with a resolver + */ + connect(params: ServeParams): Thenable; + } + + export interface ProcessExit { + readonly status: number; + readonly message?: string; + } + + export interface ReadStream { + readonly onDidReceiveMessage: Event; + readonly onEnd: Thenable; + } + + export interface WriteStream { + write(data: Uint8Array): void; + end(): void; + } + + export interface ServeParams { + readonly socketId: number; + readonly commit?: string; + readonly quality: string; + readonly extensions: string[]; + /** Whether server traffic should be compressed. */ + readonly compress?: boolean; + /** Optional explicit connection token for the server. */ + readonly connectionToken?: string; + } + + export interface CliBuild { + readonly quality: string; + /** 'LinuxAlpineX64' | 'LinuxX64' | 'LinuxARM64' | 'LinuxARM32' | 'DarwinX64' | 'DarwinARM64' | 'WindowsX64' | 'WindowsX86' | 'WindowsARM64' */ + readonly buildTarget: string; + readonly commit: string; + } + + export interface ExecEnvironment { + readonly env: ProcessEnv; + /** 'darwin' | 'linux' | 'win32' */ + readonly osPlatform: string; + /** uname.version or windows version number, undefined if it could not be read. */ + readonly osRelease?: string; + } + + export interface RemoteFileSystem { + /** + * Retrieve metadata about a file. + * + * @param path The path of the file to retrieve metadata about. + * @returns The file metadata about the file. + * @throws an exception when `path` doesn't exist. + */ + stat(path: string): Thenable; + } export interface RemoteAuthorityResolver { /** diff --git a/yarn.lock b/yarn.lock index 924eaed8282..02ee18f6e8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10685,45 +10685,45 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-canvas@0.6.0-beta.19: - version "0.6.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.19.tgz#c9e01330a548fbb243d731728e70ae77e6dc8c4f" - integrity sha512-S2tZXqnAqkLA5r40gbOlciR7CLtfztn0lk/ko8Bol08pgSqEzWKF0yadYpsezHRmemvTSmyePCtOTqDrEgFQBA== +xterm-addon-canvas@0.6.0-beta.27: + version "0.6.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.6.0-beta.27.tgz#2517f050d165b093a3c3e564e4420ccc3ccbad75" + integrity sha512-mSxEJKPnXYKkD6/zQLdNH6kB+sr4B+4DMFzntWgxLjHJdyOO95wUSAtBFnhAUez2nNYvXbs/OXpEbdVdO7f2kQ== xterm-addon-image@0.6.0-beta.21: version "0.6.0-beta.21" resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.21.tgz#e3708bc504c56a23ff31f12a2eeb335331a92aac" integrity sha512-8/PTaXVPa4kQ0xzVeuZZk10OpbZBj2cgfwhM2B0ChSPvwrk0lX+ksnXdtDKH3tg+JYvo7fIhNXtkr4NwWt7VJQ== -xterm-addon-search@0.14.0-beta.18: - version "0.14.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.18.tgz#6298b34d9c590f3e3acdd101eb297eaf8cb1f298" - integrity sha512-1CF2bPz9/vQR+q7OFgjvbBRQ0rUSkiKlwZJMnizgbKl6qx0GNg15T52J+l8zLgg8HavlF8aVps1co1A+N5PPZA== +xterm-addon-search@0.14.0-beta.26: + version "0.14.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.14.0-beta.26.tgz#2b5b8af31613c896d354c4799624090b11cd601c" + integrity sha512-CghsGO7fJa0efClbgZH20lh/JUaQYgJ1AJTPm8luc/eDc6DWOJblU0MxIABclLgT8lagv9+sOQfO0VIkAITxig== -xterm-addon-serialize@0.12.0-beta.18: - version "0.12.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.12.0-beta.18.tgz#0d9369acb49fa01f124dfff064a17f4874211f1a" - integrity sha512-RyV6iU/KRC3QN29i3iaWzm33ACbi0gMQW+LjKSFJN/XrNO1QTqfnh2VZp58G32IFG8l2sg/FUs2gXtEB5IMlhA== +xterm-addon-serialize@0.12.0-beta.26: + version "0.12.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.12.0-beta.26.tgz#cb5bd80128e82880369cb012938e14414b182aa1" + integrity sha512-b4lOcttE6lqAF3zB2l8XtDShe5djhl9SueljnVWuG4mYMYPQoiklxFcpY66sjSCIAS6NsbtrL/LGQ/0eZGi+Ig== -xterm-addon-unicode11@0.7.0-beta.18: - version "0.7.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.18.tgz#8e47b5be3ce5f07a136ca66919f548680da96648" - integrity sha512-5Zy2Kn7kSYQP2ItPV9HNsX2u6/XajB6uyRZ/tx5U79XjZIOMMtPLki56fiGxBOlyhIHFr96bwRlvYKZcEY1ndQ== +xterm-addon-unicode11@0.7.0-beta.26: + version "0.7.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.7.0-beta.26.tgz#f9606231a8f13e57dbdec5e884b044b0813931f5" + integrity sha512-po+z1ayyrkWh8IGXKpbwCLKLKfcjotZVKqowU6PtHuDtJm/J8rlzvV2eJU1WQ/8ezpopU09ibWCvaf1a7EPuxA== -xterm-addon-webgl@0.17.0-beta.18: - version "0.17.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.18.tgz#0ce7b2ed4f1aaefff0eb59dac5c0e2fbba08d9ec" - integrity sha512-F94/+Koo98fAwVr8zFw4vYnmZPKyN6K4ZQTDM2ICozBHtiVWgR2PjhBP8covD6vXwbsnrwq5aipJLbhBMeZ60w== +xterm-addon-webgl@0.17.0-beta.26: + version "0.17.0-beta.26" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.17.0-beta.26.tgz#aee4a043981d5d303b7112ef7049bc2865e75393" + integrity sha512-N8CuAPZnoDlQ6yV7n4eXQ2ONPr/GdxiwgxrJjNks4CzzHiJREm23FQIv0fCTwKQS5xU3qoc4LlT3vZ1tKGjtQw== -xterm-headless@5.4.0-beta.19: - version "5.4.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.4.0-beta.19.tgz#cdbad09917bdbeeae9197c87663d603fc2794212" - integrity sha512-lLHbZ0DUBoolt4kWCchBAZDlDDdWROwIFxzG8sK389/Z7AlVWsA7kasz2TIPN+l9SC7MrhDdWIFwi1Z0eVODcg== +xterm-headless@5.4.0-beta.27: + version "5.4.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.4.0-beta.27.tgz#cfce5f86e83580388238ea204bb451b7ffe94dc9" + integrity sha512-vdrq5eeNMyHZRDw5XR/TPl8oPln0BqbR07akt/fDXMsVg6YwWG+UOnU6GIMj7bJaBed5YkPV9NeBtdsVQn4Lyw== -xterm@5.4.0-beta.19: - version "5.4.0-beta.19" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.19.tgz#5177e37c8e885aa5cbbb0b1972e9df7634a8aa33" - integrity sha512-eM5UmMf3ml8NIBixEwH5CKj5rgwiZhE51W8xhs7js1GIGVusG/1SL7gS6d/n9UlspnAvQtUOIqzc70x887m6jQ== +xterm@5.4.0-beta.27: + version "5.4.0-beta.27" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.4.0-beta.27.tgz#f641ee045a65c9c8967fac534a202062706a8fa9" + integrity sha512-gKqtrjy0RLk2123oFyPw5tkV96jGz4c/JkY8/XUvBXoMVsX4A7rVKpHlmHhmnuK1X5ERAkvCD21YE7LfB8WYkw== y18n@^3.2.1: version "3.2.2"