diff --git a/.github/commands.json b/.github/commands.json index 15c6cd953c5..9d2ac7d7090 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -486,7 +486,7 @@ ], "action": "comment", "addLabel": "info-needed", - "comment": "Please perform the following **three tasks** to diagnose the root cause of the issue:\n\n* [ ] **1.) Disable Extensions**\n * Select `View` and pick `Command Palette...`\n * Run `Developer: Reload With Extensions Disabled`\n * šŸ‘‰ See if the issue reproduces\n\n* [ ] **2.) Disable Configuration**\n * Select `View` and pick `Command Palette...`\n * Run `Profiles: Create a Temporary Profile`\n * šŸ‘‰ See if the issue reproduces\n\n* [ ] **3.) Try VS Code Insiders**\n * Download [VS Code Insiders](https://code.visualstudio.com/insiders/)\n * Install and Run it\n * šŸ‘‰ See if the issue reproduces\n \nThen pick one of the three resolutions depending on which step has helped:\n\n
\n Disabling my Extensions helped\n\nPlease run the command `Start Extension Bisect` and follow the instructions to find the extension that is causing this issue.\n\nimage\n\nPlease report the issue to the extension causing this.\n
\n\n
\n Disabling my configuration helped\nPlease report back more details about your configuration, including settings.\n
\n\n
\n Using VS Code Insiders has helped\nāœ… This likely means that the issue has been addressed already and will be available in an upcoming release. You can safely use VS Code Insiders until the new stable version is available.\n
" + "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" }, { "__comment__": "Allows folks on the team to label issues by commenting: `\\label My-Label` ", diff --git a/.gitignore b/.gitignore index 32f514ad07a..0601e762dff 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ vscode.db /cli/target /cli/openssl product.overrides.json +*.snap.actual diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 842fe282f17..5bd52b7826e 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -124,6 +124,16 @@ "language": "github-issues", "value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified label:unreleased" }, + { + "kind": 1, + "language": "markdown", + "value": "## All Unverified Fixes" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:z-author-verified " + }, { "kind": 1, "language": "markdown", diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index d41c81d839e..96c780e5a6f 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\n\n$MILESTONE=milestone:\"August 2023\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\r\n\r\n$MILESTONE=milestone:\"August 2023\"\r\n\r\n$MINE=assignee:@me" }, { "kind": 1, @@ -169,6 +169,16 @@ "language": "github-issues", "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed -author:@me sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found" }, + { + "kind": 1, + "language": "markdown", + "value": "## Test steps needed from others" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS $MILESTONE -$MINE is:issue label:bug label:verification-steps-needed" + }, { "kind": 1, "language": "markdown", @@ -177,6 +187,6 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode $MILESTONE $MINE is:issue is:closed reason:completed label:feature-request -label:on-release-notes\nrepo:microsoft/vscode $MILESTONE $MINE is:issue is:closed reason:completed label:engineering -label:on-release-notes\nrepo:microsoft/vscode $MILESTONE $MINE is:issue is:closed reason:completed label:plan-item -label:on-release-notes" + "value": "repo:microsoft/vscode $MILESTONE $MINE is:issue is:closed reason:completed label:feature-request -label:on-release-notes\r\nrepo:microsoft/vscode $MILESTONE $MINE is:issue is:closed reason:completed label:engineering -label:on-release-notes\r\nrepo:microsoft/vscode $MILESTONE $MINE is:issue is:closed reason:completed label:plan-item -label:on-release-notes" } ] \ No newline at end of file diff --git a/.vscode/shared.code-snippets b/.vscode/shared.code-snippets index fb3df23dd42..f473425b76f 100644 --- a/.vscode/shared.code-snippets +++ b/.vscode/shared.code-snippets @@ -6,7 +6,7 @@ // Placeholders with the same ids are connected. // Example: "MSFT Copyright Header": { - "scope": "javascript,typescript,css", + "scope": "javascript,typescript,css,rust", "prefix": [ "header", "stub", diff --git a/.yarnrc b/.yarnrc index 6826ca67118..8f658afd4a9 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "25.4.0" -ms_build_id "22958381" +target "25.5.0" +ms_build_id "23084831" runtime "electron" build_from_source "true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index c0d1e51c571..f5c22bc1c23 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,27 +1,27 @@ -fc51b54078335a8d687e0dda5e65e0b9916d9678d13d8f84b1dc91bceaac6b94 *electron-v25.4.0-darwin-arm64-symbols.zip -8a1f0cc5c8fea926360347f9448a766f56fe4fea8418ce11ca5755be933dcae2 *electron-v25.4.0-darwin-arm64.zip -a221e0fb48d178940e77bb96c8bfe26fb5f4eb5b916c93d7b771d0f216e9f310 *electron-v25.4.0-darwin-x64-symbols.zip -f6e65c0d27e99121e53eb52335ef42653108bfa5cb8de06c35853c253fafc000 *electron-v25.4.0-darwin-x64.zip -e1006e2a327f23fe73c172789a9957c83e1b7563358908c0d1afb6f2a95f305c *electron-v25.4.0-linux-arm64-symbols.zip -2225b64fbe7ff602160ad371ede9a4eb79534d865dbac3a6fcc252faf0214240 *electron-v25.4.0-linux-arm64.zip -7ff71baa0e0c55728569a3a05fb1aadd079e2dd79bbc7bb4ce460c8fefe1c422 *electron-v25.4.0-linux-armv7l-symbols.zip -71469e85d3b3bbf40b698ffb3e86b96eef854c2bc63096900d1c1a606f8b9bb4 *electron-v25.4.0-linux-armv7l.zip -28a29971aa65a72ead251bb3be5135b54a1a1c0a858e80959b877aea16975acb *electron-v25.4.0-linux-x64-symbols.zip -7cce96b1f33c7f6333d24f6966bd0613e6d95a9fd3e7fba4f20d11869adc22c6 *electron-v25.4.0-linux-x64.zip -615e880460216419d9b6dfa8cdf59d067ad3f629497e4419cd723351e544645a *electron-v25.4.0-win32-arm64-pdb.zip -548ac3cbfe5a4bd1c8628aa4a73cc424116c0870ab119cf8077993b932baf51b *electron-v25.4.0-win32-arm64-symbols.zip -c51677babba862a0b98ceee992b42eaf7d2c9e8e9dfb1496d15d7381b908dc72 *electron-v25.4.0-win32-arm64.zip -9a6271c5d9eaf07fb01b38bd743b2cdc0642bcf16f9add2724eccad2aea2b3a8 *electron-v25.4.0-win32-ia32-pdb.zip -e961dd15cfde46c9765eebd4f421e17850b85c3f25e0e6891b4f55a9834805f6 *electron-v25.4.0-win32-ia32-symbols.zip -14320e1f7db40e885ee101acd39698be58745cdfe6a519e0a5316269837f5bf7 *electron-v25.4.0-win32-ia32.zip -b055eb2b0bf037fc54f8af9d76c5894ffaffe6bd24e231da7ce847048ed60c97 *electron-v25.4.0-win32-x64-pdb.zip -0d84815a05dd0dd96266808d3cefde52cdea149c5faf2abb2d4d3c028e5f30d0 *electron-v25.4.0-win32-x64-symbols.zip -6f0c6da70a4daa50ec46c389896d08009ce14cdac5896c608e5af8956fd359de *electron-v25.4.0-win32-x64.zip -0e4fd74705d098d616b313b9078e74ba3b44ead2f3abd083acd8fab68048a563 *ffmpeg-v25.4.0-darwin-arm64.zip -a4c1b013448c5b9955d4927be7a72f8836a7af909c795aeecdb43086e76bd0d8 *ffmpeg-v25.4.0-darwin-x64.zip -bd52d57ff97fb56ac01a3482af905d04f0d4e9c13c53858c6d9f99957eca82da *ffmpeg-v25.4.0-linux-arm64.zip -9b3d09177fa1e63e2a6beecfa70aeec30aeb5c1873ff21128a68051c4e23f95d *ffmpeg-v25.4.0-linux-armv7l.zip -edc7b1c9f1a0733f109a2c0375a4e40c5bfe0bf28b7f06dcc76e1ada0aa2f125 *ffmpeg-v25.4.0-linux-x64.zip -39d5389ff83fd30bacfd82e3beea5cb95eaa46aaa7d2ddaccc705f2490210f2e *ffmpeg-v25.4.0-win32-arm64.zip -059c72026437c4d97ec55ef6fab85526d2fa1de3571c478a55dec3489487fdf4 *ffmpeg-v25.4.0-win32-ia32.zip -01871604f273c3206c808954c604afda0294da15dd02dd6360fe736e5b5c82a8 *ffmpeg-v25.4.0-win32-x64.zip +c3fb8cb4804143eb25fce55a179a6f2df8c215ed709104ec235c96e357b20f42 *electron-v25.5.0-darwin-arm64-symbols.zip +e6d2a09348d4fe7c9fbd92bf796489a95e625642f0f1ce96169212554cfa6841 *electron-v25.5.0-darwin-arm64.zip +15c28e613dfee0f7e46a296bb4aed64e17f9644d7ef19129aeb6480b8da230ab *electron-v25.5.0-darwin-x64-symbols.zip +a5c5c0b621daf8242258c89edb2387fbdf1c69125c984f8564d89b87927373e3 *electron-v25.5.0-darwin-x64.zip +69d60a69b7f692b9069cdf9e518bcbaca9cec561f40199cc87196929936a34ce *electron-v25.5.0-linux-arm64-symbols.zip +adec2ba09faf5f8d8af8997c8bed43c7712eba5546db1e7d06f8357bf4613921 *electron-v25.5.0-linux-arm64.zip +0fe7a5d152c9c401671e02ebcd9da34ab9bb0c28598ede3657077a79f8b3e70a *electron-v25.5.0-linux-armv7l-symbols.zip +eccb66e4a308a0bd2d90474894370e3d687e32f78672e6b5077c1822c7bc526b *electron-v25.5.0-linux-armv7l.zip +82bd9bc9e66f8ae802fe48a51b8d7e2fb599403e9715fa4b859190200a7376b1 *electron-v25.5.0-linux-x64-symbols.zip +485cbeb206fccfb4ed42f694100eebb80c9db6639b3537a95823c4fcb7f210cd *electron-v25.5.0-linux-x64.zip +ed2c2a7da571b53bcb336b9a2a024753a272df82ece45df83df888df51bf1912 *electron-v25.5.0-win32-arm64-pdb.zip +c316f6364e9b4cd61e19d4763c96abda7247a2c31ae7d30e81219c9a7754f11a *electron-v25.5.0-win32-arm64-symbols.zip +582ccbfc5a85a093f5639ee476bb5fef18c2d25dab4a60e5f5cb47e64c99f7fb *electron-v25.5.0-win32-arm64.zip +5776e650c23e3847b0c52d850f61c57a84a4fa30943c3cc82197250112911b5b *electron-v25.5.0-win32-ia32-pdb.zip +730b429ad2c4cae0fe3ba9600a6ee86c79dada77976bac1c75ca2404993495bc *electron-v25.5.0-win32-ia32-symbols.zip +7e6e68aa33a89c0d647575b06daf415a2401feb170ebb9cd795e221af321f751 *electron-v25.5.0-win32-ia32.zip +0792665fda9255b340a829cdd24601887a2ca8f04cc49f9a6d4557db0c0cd2f2 *electron-v25.5.0-win32-x64-pdb.zip +dc2459546951f8418e866857e9111dd83a0789d401906b2200c2f8dd59d9146b *electron-v25.5.0-win32-x64-symbols.zip +9bf7980fbc024ba77ea8ab3e2d32088a5f69bf32506a7d2db72ede17028abdf4 *electron-v25.5.0-win32-x64.zip +5b1ea601b737842eacf88d7456c4d14e697822753e9a08ede889cce9e20bafb2 *ffmpeg-v25.5.0-darwin-arm64.zip +37bf5c75edefc0b6735b44d0b5b06fdd427179a8501f6e93df9840283cdb4a95 *ffmpeg-v25.5.0-darwin-x64.zip +bd52d57ff97fb56ac01a3482af905d04f0d4e9c13c53858c6d9f99957eca82da *ffmpeg-v25.5.0-linux-arm64.zip +9b3d09177fa1e63e2a6beecfa70aeec30aeb5c1873ff21128a68051c4e23f95d *ffmpeg-v25.5.0-linux-armv7l.zip +edc7b1c9f1a0733f109a2c0375a4e40c5bfe0bf28b7f06dcc76e1ada0aa2f125 *ffmpeg-v25.5.0-linux-x64.zip +2e28767b3570ea247869a20988cddd23af710eb994d6099404f123390cedeba6 *ffmpeg-v25.5.0-win32-arm64.zip +715568eefd7267573a30186ade3de901587baeb1f013200d8ae50b35941b613f *ffmpeg-v25.5.0-win32-ia32.zip +29876504452aaa505f696642178968e24b8dc8cc4b055071e6f0f3f073088acc *ffmpeg-v25.5.0-win32-x64.zip diff --git a/build/hygiene.js b/build/hygiene.js index b8881081b2e..2c01b1f4d75 100644 --- a/build/hygiene.js +++ b/build/hygiene.js @@ -150,10 +150,12 @@ function hygiene(some, linting = true) { } const productJsonFilter = filter('product.json', { restore: true }); + const snapshotFilter = filter(['**', '!**/*.snap', '!**/*.snap.actual']); const unicodeFilterStream = filter(unicodeFilter, { restore: true }); const result = input .pipe(filter((f) => !f.stat.isDirectory())) + .pipe(snapshotFilter) .pipe(productJsonFilter) .pipe(process.env['BUILD_SOURCEVERSION'] ? es.through() : productJson) .pipe(productJsonFilter.restore) diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 2270e054ba5..8e82e1cd234 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -82,17 +82,19 @@ function createCompile(src, build, emitError, transpileOnly) { return pipeline; } function transpileTask(src, out, swc) { - return function () { + const task = () => { const transpile = createCompile(src, false, true, { swc }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); return srcPipe .pipe(transpile()) .pipe(gulp.dest(out)); }; + task.taskName = `transpile-${path.basename(src)}`; + return task; } exports.transpileTask = transpileTask; function compileTask(src, out, build, options = {}) { - return function () { + const task = () => { if (os.totalmem() < 4000000000) { throw new Error('compilation requires 4GB of RAM'); } @@ -128,10 +130,12 @@ function compileTask(src, out, build, options = {}) { .pipe(compile()) .pipe(gulp.dest(out)); }; + task.taskName = `compile-${path.basename(src)}`; + return task; } exports.compileTask = compileTask; function watchTask(out, build) { - return function () { + const task = () => { const compile = createCompile('src', build, false, false); const src = gulp.src('src/**', { base: 'src' }); const watchSrc = watch('src/**', { base: 'src', readDelay: 200 }); @@ -142,6 +146,8 @@ function watchTask(out, build) { .pipe(util.incremental(compile, src, true)) .pipe(gulp.dest(out)); }; + task.taskName = `watch-${path.basename(out)}`; + return task; } exports.watchTask = watchTask; const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); @@ -281,4 +287,4 @@ exports.watchApiProposalNamesTask = task.define('watch-api-proposal-names', () = .pipe(util.debounce(task)) .pipe(gulp.dest('src')); }); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tcGlsYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjb21waWxhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7OztBQUVoRyxtQ0FBbUM7QUFDbkMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3Qiw2QkFBNkI7QUFDN0IsMENBQTBDO0FBQzFDLDZCQUE2QjtBQUM3Qix5Q0FBNEM7QUFDNUMsK0JBQStCO0FBQy9CLHNDQUFzQztBQUN0QywwQ0FBMEM7QUFDMUMseUJBQXlCO0FBQ3pCLGlDQUFrQztBQUNsQyw4QkFBOEI7QUFDOUIsK0JBQStCO0FBQy9CLDBDQUF5QztBQUV6QyxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUM7QUFHakMsdUVBQXVFO0FBRXZFLE1BQU0sUUFBUSxHQUFHLElBQUEseUJBQWMsR0FBRSxDQUFDO0FBRWxDLFNBQVMsNEJBQTRCLENBQUMsR0FBVztJQUNoRCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxTQUFTLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDckQsTUFBTSxPQUFPLEdBQXVCLEVBQUUsQ0FBQztJQUN2QyxPQUFPLENBQUMsT0FBTyxHQUFHLEtBQUssQ0FBQztJQUN4QixPQUFPLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQztJQUN6QixJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMscUJBQXFCLENBQUMsRUFBRSxFQUFFLHNDQUFzQztRQUMvRSxPQUFPLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQztLQUMxQjtJQUNELE9BQU8sQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO0lBQzFCLE9BQU8sQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO0lBQzFCLE9BQU8sQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUM3QyxPQUFPLENBQUMsT0FBTyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxVQUFVLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDM0UsT0FBTyxPQUFPLENBQUM7QUFDaEIsQ0FBQztBQUVELFNBQVMsYUFBYSxDQUFDLEdBQVcsRUFBRSxLQUFjLEVBQUUsU0FBa0IsRUFBRSxhQUF5QztJQUNoSCxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUEyQixDQUFDO0lBQ3ZELE1BQU0sVUFBVSxHQUFHLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBcUMsQ0FBQztJQUdsRixNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxRQUFRLEVBQUUsR0FBRyxFQUFFLGVBQWUsQ0FBQyxDQUFDO0lBQ3pFLE1BQU0sZUFBZSxHQUFHLEVBQUUsR0FBRyw0QkFBNEIsQ0FBQyxHQUFHLENBQUMsRUFBRSxhQUFhLEVBQUUsT0FBTyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7SUFDaEcsSUFBSSxDQUFDLEtBQUssRUFBRTtRQUNYLGVBQWUsQ0FBQyxlQUFlLEdBQUcsSUFBSSxDQUFDO0tBQ3ZDO0lBRUQsTUFBTSxXQUFXLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxXQUFXLEVBQUUsZUFBZSxFQUFFO1FBQzVELE9BQU8sRUFBRSxLQUFLO1FBQ2QsYUFBYSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUM7UUFDckMsZ0JBQWdCLEVBQUUsT0FBTyxhQUFhLEtBQUssU0FBUyxJQUFJLGFBQWEsQ0FBQyxHQUFHO0tBQ3pFLEVBQUUsR0FBRyxDQUFDLEVBQUUsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUV6QixTQUFTLFFBQVEsQ0FBQyxLQUErQjtRQUNoRCxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsVUFBVSxDQUE4QixDQUFDO1FBRTdELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQzlELE1BQU0sVUFBVSxHQUFHLENBQUMsQ0FBTyxFQUFFLEVBQUUsQ0FBQywwQkFBMEIsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3hFLE1BQU0sV0FBVyxHQUFHLENBQUMsQ0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ3hGLE1BQU0sb0JBQW9CLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFFaEYsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQzNCLE1BQU0sTUFBTSxHQUFHLEtBQUs7YUFDbEIsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsVUFBVSxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsQ0FBQyx5RUFBeUU7YUFDM0csSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxLQUFLLElBQUksV0FBVyxFQUFFLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxDQUFDLENBQUM7YUFDcEUsSUFBSSxDQUFDLFFBQVEsQ0FBQzthQUNkLElBQUksQ0FBQyxJQUFJLENBQUMsY0FBYyxFQUFFLENBQUM7YUFDM0IsSUFBSSxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUMsQ0FBQzthQUN4QixJQUFJLENBQUMsb0JBQW9CLENBQUM7YUFDMUIsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxDQUFDO2FBQ2hDLElBQUksQ0FBQyxvQkFBb0IsQ0FBQyxPQUFPLENBQUM7YUFDbEMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxhQUFhLEVBQUUsVUFBVSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDcEQsVUFBVSxFQUFFLEtBQUs7WUFDakIsY0FBYyxFQUFFLENBQUMsQ0FBQyxLQUFLO1lBQ3ZCLFVBQVUsRUFBRSxlQUFlLENBQUMsVUFBVTtTQUN0QyxDQUFDLENBQUMsQ0FBQzthQUNILElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDO2FBQ3RCLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO1FBRWxDLE9BQU8sRUFBRSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDakMsQ0FBQztJQUNELFFBQVEsQ0FBQyxZQUFZLEdBQUcsR0FBRyxFQUFFO1FBQzVCLE9BQU8sV0FBVyxDQUFDLEdBQUcsQ0FBQyxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO0lBQ3ZDLENBQUMsQ0FBQztJQUNGLFFBQVEsQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO0lBQ25DLE9BQU8sUUFBUSxDQUFDO0FBQ2pCLENBQUM7QUFFRCxTQUFnQixhQUFhLENBQUMsR0FBVyxFQUFFLEdBQVcsRUFBRSxHQUFZO0lBRW5FLE9BQU87UUFFTixNQUFNLFNBQVMsR0FBRyxhQUFhLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO1FBQzNELE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxHQUFHLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxHQUFHLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUUxRCxPQUFPLE9BQU87YUFDWixJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7YUFDakIsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4QixDQUFDLENBQUM7QUFDSCxDQUFDO0FBWEQsc0NBV0M7QUFFRCxTQUFnQixXQUFXLENBQUMsR0FBVyxFQUFFLEdBQVcsRUFBRSxLQUFjLEVBQUUsVUFBdUMsRUFBRTtJQUU5RyxPQUFPO1FBRU4sSUFBSSxFQUFFLENBQUMsUUFBUSxFQUFFLEdBQUcsVUFBYSxFQUFFO1lBQ2xDLE1BQU0sSUFBSSxLQUFLLENBQUMsaUNBQWlDLENBQUMsQ0FBQztTQUNuRDtRQUVELE1BQU0sT0FBTyxHQUFHLGFBQWEsQ0FBQyxHQUFHLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQztRQUN2RCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsR0FBRyxLQUFLLEVBQUUsRUFBRSxJQUFJLEVBQUUsR0FBRyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDMUQsTUFBTSxTQUFTLEdBQUcsSUFBSSxlQUFlLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDN0MsSUFBSSxHQUFHLEtBQUssS0FBSyxFQUFFO1lBQ2xCLFNBQVMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztTQUNwQjtRQUVELG1DQUFtQztRQUNuQyxJQUFJLFlBQVksR0FBRyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDaEMsSUFBSSxLQUFLLElBQUksQ0FBQyxPQUFPLENBQUMsYUFBYSxFQUFFO1lBQ3BDLElBQUksWUFBWSxHQUFHLElBQUksZUFBTyxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsQ0FBQyxHQUFHLElBQUksRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLEVBQUUsR0FBRyxJQUFJLENBQUMsRUFBRSxFQUFFLGFBQWEsRUFBRSxJQUFJLEVBQUUsbUJBQW1CLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztZQUN0SyxNQUFNLHFCQUFxQixHQUFHLFlBQVksQ0FBQyxzQkFBc0IsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUMxRixZQUFZLEdBQUcsRUFBRSxDQUFDLE9BQU8sQ0FBQyxLQUFLLFVBQVUsS0FBSyxDQUFDLElBQXlDO2dCQUV2RixNQUFNLFlBQVksR0FBbUIsRUFBRyxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQ2xFLE1BQU0sV0FBVyxHQUFHLENBQUMsTUFBTSxxQkFBcUIsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxZQUFZLENBQUMsQ0FBQztnQkFDcEUsSUFBSSxXQUFXLEtBQUssU0FBUyxFQUFFO29CQUM5QixJQUFJLENBQUMsUUFBUSxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO29CQUM3QyxJQUFJLENBQUMsU0FBUyxHQUFHLFdBQVcsQ0FBQyxTQUFTLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxXQUFXLENBQUMsU0FBUyxDQUFDLENBQUM7aUJBQzVFO2dCQUNELElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDakIsQ0FBQyxFQUFFLEtBQUssVUFBVSxHQUFHO2dCQUNwQixpQkFBaUI7Z0JBQ2pCLENBQUMsTUFBTSxxQkFBcUIsQ0FBQyxDQUFDLEtBQUssRUFBRSxDQUFDO2dCQUV0QyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO2dCQUNWLFlBQWEsR0FBRyxTQUFTLENBQUM7WUFDakMsQ0FBQyxDQUFDLENBQUM7U0FDSDtRQUVELE9BQU8sT0FBTzthQUNaLElBQUksQ0FBQyxZQUFZLENBQUM7YUFDbEIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUM7YUFDdEIsSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDO2FBQ2YsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4QixDQUFDLENBQUM7QUFDSCxDQUFDO0FBNUNELGtDQTRDQztBQUVELFNBQWdCLFNBQVMsQ0FBQyxHQUFXLEVBQUUsS0FBYztJQUVwRCxPQUFPO1FBQ04sTUFBTSxPQUFPLEdBQUcsYUFBYSxDQUFDLEtBQUssRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBRTFELE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7UUFDaEQsTUFBTSxRQUFRLEdBQUcsS0FBSyxDQUFDLFFBQVEsRUFBRSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsU0FBUyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUM7UUFFbEUsTUFBTSxTQUFTLEdBQUcsSUFBSSxlQUFlLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDNUMsU0FBUyxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBRXBCLE9BQU8sUUFBUTthQUNiLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDO2FBQ3RCLElBQUksQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLE9BQU8sRUFBRSxHQUFHLEVBQUUsSUFBSSxDQUFDLENBQUM7YUFDMUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4QixDQUFDLENBQUM7QUFDSCxDQUFDO0FBaEJELDhCQWdCQztBQUVELE1BQU0sZUFBZSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFFLFdBQVcsQ0FBQyxDQUFDO0FBRTFELE1BQU0sZUFBZTtJQUNILFFBQVEsQ0FBVTtJQUNuQixNQUFNLENBQXlCO0lBRTlCLGFBQWEsQ0FBa0M7SUFDL0MsV0FBVyxDQUF1QjtJQUNsQyxvQkFBb0IsQ0FBZ0M7SUFFckUsWUFBWSxPQUFnQjtRQUMzQixJQUFJLENBQUMsUUFBUSxHQUFHLE9BQU8sQ0FBQztRQUN4QixJQUFJLENBQUMsTUFBTSxHQUFHLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUMzQixJQUFJLENBQUMsYUFBYSxHQUFHLEVBQUUsQ0FBQztRQUN4QixNQUFNLGNBQWMsR0FBRyxDQUFDLFFBQWdCLEVBQUUsUUFBZ0IsRUFBRSxFQUFFO1lBQzdELElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFO2dCQUNuQixPQUFPO2FBQ1A7WUFDRCxJQUFJLElBQUksQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLEVBQUU7Z0JBQ2pDLE9BQU87YUFDUDtZQUNELElBQUksQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLEdBQUcsSUFBSSxDQUFDO1lBRXBDLEVBQUUsQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLEdBQUcsRUFBRTtnQkFDM0IsSUFBSSxDQUFDLG9CQUFvQixDQUFDLGVBQWUsQ0FBQyxRQUFRLENBQUMsQ0FBQztnQkFDcEQsSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1lBQ3JCLENBQUMsQ0FBQyxDQUFDO1FBQ0osQ0FBQyxDQUFDO1FBQ0YsSUFBSSxDQUFDLFdBQVcsR0FBRyxJQUFJLEtBQU0sU0FBUSxTQUFTLENBQUMsVUFBVTtZQUNqRCxZQUFZLENBQUMsUUFBZ0IsRUFBRSxRQUFnQjtnQkFDckQsY0FBYyxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztnQkFDbkMsT0FBTyxLQUFLLENBQUMsWUFBWSxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztZQUMvQyxDQUFDO1NBQ0QsQ0FBQztRQUNGLElBQUksQ0FBQyxvQkFBb0IsR0FBRyxJQUFJLFNBQVMsQ0FBQyxtQkFBbUIsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLENBQUM7UUFFaEYsSUFBSSxJQUFJLENBQUMsUUFBUSxFQUFFO1lBQ2xCLEVBQUUsQ0FBQyxTQUFTLENBQUMsU0FBUyxDQUFDLFdBQVcsRUFBRSxHQUFHLEVBQUU7Z0JBQ3hDLElBQUksQ0FBQyxZQUFZLEVBQUUsQ0FBQztZQUNyQixDQUFDLENBQUMsQ0FBQztTQUNIO0lBQ0YsQ0FBQztJQUVPLGlCQUFpQixHQUF3QixJQUFJLENBQUM7SUFDOUMsWUFBWTtRQUNuQixJQUFJLElBQUksQ0FBQyxpQkFBaUIsS0FBSyxJQUFJLEVBQUU7WUFDcEMsWUFBWSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO1lBQ3JDLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxJQUFJLENBQUM7U0FDOUI7UUFDRCxJQUFJLENBQUMsaUJBQWlCLEdBQUcsVUFBVSxDQUFDLEdBQUcsRUFBRTtZQUN4QyxJQUFJLENBQUMsaUJBQWlCLEdBQUcsSUFBSSxDQUFDO1lBQzlCLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNoQixDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDUixDQUFDO0lBRU8sSUFBSTtRQUNYLE1BQU0sQ0FBQyxHQUFHLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLG9CQUFvQixDQUFDLENBQUM7UUFDcEQsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUU7WUFDekIsNERBQTREO1lBQzVELE1BQU0sSUFBSSxLQUFLLENBQUMsZ0RBQWdELENBQUMsQ0FBQztTQUNsRTtRQUNELE9BQU8sQ0FBQyxDQUFDO0lBQ1YsQ0FBQztJQUVPLElBQUksQ0FBQyxPQUFZLEVBQUUsR0FBRyxJQUFXO1FBQ3hDLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLGVBQWUsQ0FBQyxFQUFFLE9BQU8sRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0lBQzlELENBQUM7SUFFTSxPQUFPO1FBQ2IsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQzdCLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUMzQixJQUFJLENBQUMsTUFBTSxFQUFFO1lBQ1oseUJBQXlCO1lBQ3pCLE9BQU87U0FDUDtRQUNELElBQUksTUFBTSxDQUFDLFNBQVMsRUFBRTtZQUNyQixPQUFPO1NBQ1A7UUFFRCxFQUFFLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2xELEVBQUUsQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxlQUFlLEVBQUUsZ0RBQWdELENBQUMsRUFBRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDN0csSUFBSSxDQUFDLElBQUksQ0FBQyw0Q0FBNEMsSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLFNBQVMsS0FBSyxDQUFDLENBQUM7UUFDbkYsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUU7WUFDbkIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLHFGQUFxRixDQUFDLENBQUM7U0FDakg7SUFDRixDQUFDO0NBQ0Q7QUFFRCxTQUFTLHdCQUF3QjtJQUNoQyxJQUFJLEdBQVcsQ0FBQztJQUVoQixJQUFJO1FBQ0gsTUFBTSxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyx1RUFBdUUsRUFBRSxPQUFPLENBQUMsQ0FBQztRQUM5RyxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ2pDLEdBQUcsR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEdBQUcsQ0FBQztLQUNoQztJQUFDLE1BQU07UUFDUCxHQUFHLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQztLQUNiO0lBRUQsTUFBTSxPQUFPLEdBQUcsdUNBQXVDLENBQUM7SUFDeEQsTUFBTSxhQUFhLEdBQUcsSUFBSSxHQUFHLEVBQVUsQ0FBQztJQUV4QyxNQUFNLEtBQUssR0FBRyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDM0IsTUFBTSxNQUFNLEdBQUcsS0FBSztTQUNsQixJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQU8sRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztTQUNwRCxJQUFJLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQU8sRUFBRSxFQUFFO1FBQzVCLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ25DLE1BQU0sS0FBSyxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFFakMsSUFBSSxLQUFLLEVBQUU7WUFDVixhQUFhLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQzVCO0lBQ0YsQ0FBQyxFQUFFO1FBQ0YsTUFBTSxLQUFLLEdBQUcsQ0FBQyxHQUFHLGFBQWEsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO1FBQ2pELE1BQU0sUUFBUSxHQUFHO1lBQ2hCLGlHQUFpRztZQUNqRywrREFBK0Q7WUFDL0Qsa0dBQWtHO1lBQ2xHLGtHQUFrRztZQUNsRyxFQUFFO1lBQ0Ysb0RBQW9EO1lBQ3BELEVBQUU7WUFDRixnREFBZ0Q7WUFDaEQsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsS0FBSyxJQUFJLDZGQUE2RixJQUFJLFFBQVEsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLEdBQUcsRUFBRSxDQUFDLEVBQUU7WUFDMUosS0FBSztZQUNMLDZEQUE2RDtZQUM3RCxFQUFFO1NBQ0YsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUM7UUFFWixJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLElBQUksQ0FBQztZQUMxQixJQUFJLEVBQUUsbUVBQW1FO1lBQ3pFLFFBQVEsRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQztTQUMvQixDQUFDLENBQUMsQ0FBQztRQUNKLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDbEIsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUVMLE9BQU8sRUFBRSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUM7QUFDakMsQ0FBQztBQUVELE1BQU0sd0JBQXdCLEdBQUcsSUFBQSx5QkFBYyxFQUFDLG9CQUFvQixDQUFDLENBQUM7QUFFekQsUUFBQSwyQkFBMkIsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLDRCQUE0QixFQUFFLEdBQUcsRUFBRTtJQUN6RixPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsbUJBQW1CLENBQUM7U0FDbEMsSUFBSSxDQUFDLHdCQUF3QixFQUFFLENBQUM7U0FDaEMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7U0FDdEIsSUFBSSxDQUFDLHdCQUF3QixDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0FBQzVDLENBQUMsQ0FBQyxDQUFDO0FBRVUsUUFBQSx5QkFBeUIsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLDBCQUEwQixFQUFFLEdBQUcsRUFBRTtJQUNyRixNQUFNLElBQUksR0FBRyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLG1CQUFtQixDQUFDO1NBQzlDLElBQUksQ0FBQyx3QkFBd0IsRUFBRSxDQUFDO1NBQ2hDLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUUzQyxPQUFPLEtBQUssQ0FBQyxtQkFBbUIsRUFBRSxFQUFFLFNBQVMsRUFBRSxHQUFHLEVBQUUsQ0FBQztTQUNuRCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQztTQUN6QixJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO0FBQzFCLENBQUMsQ0FBQyxDQUFDIn0= \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tcGlsYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjb21waWxhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7OztBQUVoRyxtQ0FBbUM7QUFDbkMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3Qiw2QkFBNkI7QUFDN0IsMENBQTBDO0FBQzFDLDZCQUE2QjtBQUM3Qix5Q0FBNEM7QUFDNUMsK0JBQStCO0FBQy9CLHNDQUFzQztBQUN0QywwQ0FBMEM7QUFDMUMseUJBQXlCO0FBQ3pCLGlDQUFrQztBQUNsQyw4QkFBOEI7QUFDOUIsK0JBQStCO0FBQy9CLDBDQUF5QztBQUV6QyxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUM7QUFHakMsdUVBQXVFO0FBRXZFLE1BQU0sUUFBUSxHQUFHLElBQUEseUJBQWMsR0FBRSxDQUFDO0FBRWxDLFNBQVMsNEJBQTRCLENBQUMsR0FBVztJQUNoRCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxTQUFTLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDckQsTUFBTSxPQUFPLEdBQXVCLEVBQUUsQ0FBQztJQUN2QyxPQUFPLENBQUMsT0FBTyxHQUFHLEtBQUssQ0FBQztJQUN4QixPQUFPLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQztJQUN6QixJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMscUJBQXFCLENBQUMsRUFBRSxFQUFFLHNDQUFzQztRQUMvRSxPQUFPLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQztLQUMxQjtJQUNELE9BQU8sQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO0lBQzFCLE9BQU8sQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO0lBQzFCLE9BQU8sQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUM3QyxPQUFPLENBQUMsT0FBTyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxVQUFVLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDM0UsT0FBTyxPQUFPLENBQUM7QUFDaEIsQ0FBQztBQUVELFNBQVMsYUFBYSxDQUFDLEdBQVcsRUFBRSxLQUFjLEVBQUUsU0FBa0IsRUFBRSxhQUF5QztJQUNoSCxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUEyQixDQUFDO0lBQ3ZELE1BQU0sVUFBVSxHQUFHLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBcUMsQ0FBQztJQUdsRixNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxRQUFRLEVBQUUsR0FBRyxFQUFFLGVBQWUsQ0FBQyxDQUFDO0lBQ3pFLE1BQU0sZUFBZSxHQUFHLEVBQUUsR0FBRyw0QkFBNEIsQ0FBQyxHQUFHLENBQUMsRUFBRSxhQUFhLEVBQUUsT0FBTyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7SUFDaEcsSUFBSSxDQUFDLEtBQUssRUFBRTtRQUNYLGVBQWUsQ0FBQyxlQUFlLEdBQUcsSUFBSSxDQUFDO0tBQ3ZDO0lBRUQsTUFBTSxXQUFXLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxXQUFXLEVBQUUsZUFBZSxFQUFFO1FBQzVELE9BQU8sRUFBRSxLQUFLO1FBQ2QsYUFBYSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUM7UUFDckMsZ0JBQWdCLEVBQUUsT0FBTyxhQUFhLEtBQUssU0FBUyxJQUFJLGFBQWEsQ0FBQyxHQUFHO0tBQ3pFLEVBQUUsR0FBRyxDQUFDLEVBQUUsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUV6QixTQUFTLFFBQVEsQ0FBQyxLQUErQjtRQUNoRCxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsVUFBVSxDQUE4QixDQUFDO1FBRTdELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQzlELE1BQU0sVUFBVSxHQUFHLENBQUMsQ0FBTyxFQUFFLEVBQUUsQ0FBQywwQkFBMEIsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3hFLE1BQU0sV0FBVyxHQUFHLENBQUMsQ0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ3hGLE1BQU0sb0JBQW9CLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFFaEYsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQzNCLE1BQU0sTUFBTSxHQUFHLEtBQUs7YUFDbEIsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsVUFBVSxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsQ0FBQyx5RUFBeUU7YUFDM0csSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxLQUFLLElBQUksV0FBVyxFQUFFLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxDQUFDLENBQUM7YUFDcEUsSUFBSSxDQUFDLFFBQVEsQ0FBQzthQUNkLElBQUksQ0FBQyxJQUFJLENBQUMsY0FBYyxFQUFFLENBQUM7YUFDM0IsSUFBSSxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUMsQ0FBQzthQUN4QixJQUFJLENBQUMsb0JBQW9CLENBQUM7YUFDMUIsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxDQUFDO2FBQ2hDLElBQUksQ0FBQyxvQkFBb0IsQ0FBQyxPQUFPLENBQUM7YUFDbEMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxhQUFhLEVBQUUsVUFBVSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDcEQsVUFBVSxFQUFFLEtBQUs7WUFDakIsY0FBYyxFQUFFLENBQUMsQ0FBQyxLQUFLO1lBQ3ZCLFVBQVUsRUFBRSxlQUFlLENBQUMsVUFBVTtTQUN0QyxDQUFDLENBQUMsQ0FBQzthQUNILElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDO2FBQ3RCLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO1FBRWxDLE9BQU8sRUFBRSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDakMsQ0FBQztJQUNELFFBQVEsQ0FBQyxZQUFZLEdBQUcsR0FBRyxFQUFFO1FBQzVCLE9BQU8sV0FBVyxDQUFDLEdBQUcsQ0FBQyxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO0lBQ3ZDLENBQUMsQ0FBQztJQUNGLFFBQVEsQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO0lBQ25DLE9BQU8sUUFBUSxDQUFDO0FBQ2pCLENBQUM7QUFFRCxTQUFnQixhQUFhLENBQUMsR0FBVyxFQUFFLEdBQVcsRUFBRSxHQUFZO0lBRW5FLE1BQU0sSUFBSSxHQUFHLEdBQUcsRUFBRTtRQUVqQixNQUFNLFNBQVMsR0FBRyxhQUFhLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO1FBQzNELE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxHQUFHLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxHQUFHLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUUxRCxPQUFPLE9BQU87YUFDWixJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7YUFDakIsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4QixDQUFDLENBQUM7SUFFRixJQUFJLENBQUMsUUFBUSxHQUFHLGFBQWEsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO0lBQ2xELE9BQU8sSUFBSSxDQUFDO0FBQ2IsQ0FBQztBQWRELHNDQWNDO0FBRUQsU0FBZ0IsV0FBVyxDQUFDLEdBQVcsRUFBRSxHQUFXLEVBQUUsS0FBYyxFQUFFLFVBQXVDLEVBQUU7SUFFOUcsTUFBTSxJQUFJLEdBQUcsR0FBRyxFQUFFO1FBRWpCLElBQUksRUFBRSxDQUFDLFFBQVEsRUFBRSxHQUFHLFVBQWEsRUFBRTtZQUNsQyxNQUFNLElBQUksS0FBSyxDQUFDLGlDQUFpQyxDQUFDLENBQUM7U0FDbkQ7UUFFRCxNQUFNLE9BQU8sR0FBRyxhQUFhLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDdkQsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEdBQUcsS0FBSyxFQUFFLEVBQUUsSUFBSSxFQUFFLEdBQUcsR0FBRyxFQUFFLEVBQUUsQ0FBQyxDQUFDO1FBQzFELE1BQU0sU0FBUyxHQUFHLElBQUksZUFBZSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzdDLElBQUksR0FBRyxLQUFLLEtBQUssRUFBRTtZQUNsQixTQUFTLENBQUMsT0FBTyxFQUFFLENBQUM7U0FDcEI7UUFFRCxtQ0FBbUM7UUFDbkMsSUFBSSxZQUFZLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ2hDLElBQUksS0FBSyxJQUFJLENBQUMsT0FBTyxDQUFDLGFBQWEsRUFBRTtZQUNwQyxJQUFJLFlBQVksR0FBRyxJQUFJLGVBQU8sQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLENBQUMsR0FBRyxJQUFJLEVBQUUsRUFBRSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxFQUFFLEdBQUcsSUFBSSxDQUFDLEVBQUUsRUFBRSxhQUFhLEVBQUUsSUFBSSxFQUFFLG1CQUFtQixFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7WUFDdEssTUFBTSxxQkFBcUIsR0FBRyxZQUFZLENBQUMsc0JBQXNCLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDMUYsWUFBWSxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxVQUFVLEtBQUssQ0FBQyxJQUF5QztnQkFFdkYsTUFBTSxZQUFZLEdBQW1CLEVBQUcsQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO2dCQUNsRSxNQUFNLFdBQVcsR0FBRyxDQUFDLE1BQU0scUJBQXFCLENBQUMsQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDLENBQUM7Z0JBQ3BFLElBQUksV0FBVyxLQUFLLFNBQVMsRUFBRTtvQkFDOUIsSUFBSSxDQUFDLFFBQVEsR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQztvQkFDN0MsSUFBSSxDQUFDLFNBQVMsR0FBRyxXQUFXLENBQUMsU0FBUyxJQUFJLElBQUksQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLFNBQVMsQ0FBQyxDQUFDO2lCQUM1RTtnQkFDRCxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ2pCLENBQUMsRUFBRSxLQUFLLFVBQVUsR0FBRztnQkFDcEIsaUJBQWlCO2dCQUNqQixDQUFDLE1BQU0scUJBQXFCLENBQUMsQ0FBQyxLQUFLLEVBQUUsQ0FBQztnQkFFdEMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztnQkFDVixZQUFhLEdBQUcsU0FBUyxDQUFDO1lBQ2pDLENBQUMsQ0FBQyxDQUFDO1NBQ0g7UUFFRCxPQUFPLE9BQU87YUFDWixJQUFJLENBQUMsWUFBWSxDQUFDO2FBQ2xCLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDO2FBQ3RCLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQzthQUNmLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDeEIsQ0FBQyxDQUFDO0lBRUYsSUFBSSxDQUFDLFFBQVEsR0FBRyxXQUFXLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztJQUNoRCxPQUFPLElBQUksQ0FBQztBQUNiLENBQUM7QUEvQ0Qsa0NBK0NDO0FBRUQsU0FBZ0IsU0FBUyxDQUFDLEdBQVcsRUFBRSxLQUFjO0lBRXBELE1BQU0sSUFBSSxHQUFHLEdBQUcsRUFBRTtRQUNqQixNQUFNLE9BQU8sR0FBRyxhQUFhLENBQUMsS0FBSyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFFMUQsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxRQUFRLEVBQUUsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztRQUNoRCxNQUFNLFFBQVEsR0FBRyxLQUFLLENBQUMsUUFBUSxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxTQUFTLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQztRQUVsRSxNQUFNLFNBQVMsR0FBRyxJQUFJLGVBQWUsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM1QyxTQUFTLENBQUMsT0FBTyxFQUFFLENBQUM7UUFFcEIsT0FBTyxRQUFRO2FBQ2IsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUM7YUFDdEIsSUFBSSxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsT0FBTyxFQUFFLEdBQUcsRUFBRSxJQUFJLENBQUMsQ0FBQzthQUMxQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQ3hCLENBQUMsQ0FBQztJQUNGLElBQUksQ0FBQyxRQUFRLEdBQUcsU0FBUyxJQUFJLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7SUFDOUMsT0FBTyxJQUFJLENBQUM7QUFDYixDQUFDO0FBbEJELDhCQWtCQztBQUVELE1BQU0sZUFBZSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFFLFdBQVcsQ0FBQyxDQUFDO0FBRTFELE1BQU0sZUFBZTtJQUNILFFBQVEsQ0FBVTtJQUNuQixNQUFNLENBQXlCO0lBRTlCLGFBQWEsQ0FBa0M7SUFDL0MsV0FBVyxDQUF1QjtJQUNsQyxvQkFBb0IsQ0FBZ0M7SUFFckUsWUFBWSxPQUFnQjtRQUMzQixJQUFJLENBQUMsUUFBUSxHQUFHLE9BQU8sQ0FBQztRQUN4QixJQUFJLENBQUMsTUFBTSxHQUFHLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUMzQixJQUFJLENBQUMsYUFBYSxHQUFHLEVBQUUsQ0FBQztRQUN4QixNQUFNLGNBQWMsR0FBRyxDQUFDLFFBQWdCLEVBQUUsUUFBZ0IsRUFBRSxFQUFFO1lBQzdELElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFO2dCQUNuQixPQUFPO2FBQ1A7WUFDRCxJQUFJLElBQUksQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLEVBQUU7Z0JBQ2pDLE9BQU87YUFDUDtZQUNELElBQUksQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLEdBQUcsSUFBSSxDQUFDO1lBRXBDLEVBQUUsQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLEdBQUcsRUFBRTtnQkFDM0IsSUFBSSxDQUFDLG9CQUFvQixDQUFDLGVBQWUsQ0FBQyxRQUFRLENBQUMsQ0FBQztnQkFDcEQsSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1lBQ3JCLENBQUMsQ0FBQyxDQUFDO1FBQ0osQ0FBQyxDQUFDO1FBQ0YsSUFBSSxDQUFDLFdBQVcsR0FBRyxJQUFJLEtBQU0sU0FBUSxTQUFTLENBQUMsVUFBVTtZQUNqRCxZQUFZLENBQUMsUUFBZ0IsRUFBRSxRQUFnQjtnQkFDckQsY0FBYyxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztnQkFDbkMsT0FBTyxLQUFLLENBQUMsWUFBWSxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztZQUMvQyxDQUFDO1NBQ0QsQ0FBQztRQUNGLElBQUksQ0FBQyxvQkFBb0IsR0FBRyxJQUFJLFNBQVMsQ0FBQyxtQkFBbUIsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLENBQUM7UUFFaEYsSUFBSSxJQUFJLENBQUMsUUFBUSxFQUFFO1lBQ2xCLEVBQUUsQ0FBQyxTQUFTLENBQUMsU0FBUyxDQUFDLFdBQVcsRUFBRSxHQUFHLEVBQUU7Z0JBQ3hDLElBQUksQ0FBQyxZQUFZLEVBQUUsQ0FBQztZQUNyQixDQUFDLENBQUMsQ0FBQztTQUNIO0lBQ0YsQ0FBQztJQUVPLGlCQUFpQixHQUF3QixJQUFJLENBQUM7SUFDOUMsWUFBWTtRQUNuQixJQUFJLElBQUksQ0FBQyxpQkFBaUIsS0FBSyxJQUFJLEVBQUU7WUFDcEMsWUFBWSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO1lBQ3JDLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxJQUFJLENBQUM7U0FDOUI7UUFDRCxJQUFJLENBQUMsaUJBQWlCLEdBQUcsVUFBVSxDQUFDLEdBQUcsRUFBRTtZQUN4QyxJQUFJLENBQUMsaUJBQWlCLEdBQUcsSUFBSSxDQUFDO1lBQzlCLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNoQixDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDUixDQUFDO0lBRU8sSUFBSTtRQUNYLE1BQU0sQ0FBQyxHQUFHLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLG9CQUFvQixDQUFDLENBQUM7UUFDcEQsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUU7WUFDekIsNERBQTREO1lBQzVELE1BQU0sSUFBSSxLQUFLLENBQUMsZ0RBQWdELENBQUMsQ0FBQztTQUNsRTtRQUNELE9BQU8sQ0FBQyxDQUFDO0lBQ1YsQ0FBQztJQUVPLElBQUksQ0FBQyxPQUFZLEVBQUUsR0FBRyxJQUFXO1FBQ3hDLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLGVBQWUsQ0FBQyxFQUFFLE9BQU8sRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0lBQzlELENBQUM7SUFFTSxPQUFPO1FBQ2IsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQzdCLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUMzQixJQUFJLENBQUMsTUFBTSxFQUFFO1lBQ1oseUJBQXlCO1lBQ3pCLE9BQU87U0FDUDtRQUNELElBQUksTUFBTSxDQUFDLFNBQVMsRUFBRTtZQUNyQixPQUFPO1NBQ1A7UUFFRCxFQUFFLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2xELEVBQUUsQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxlQUFlLEVBQUUsZ0RBQWdELENBQUMsRUFBRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDN0csSUFBSSxDQUFDLElBQUksQ0FBQyw0Q0FBNEMsSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLFNBQVMsS0FBSyxDQUFDLENBQUM7UUFDbkYsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUU7WUFDbkIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLHFGQUFxRixDQUFDLENBQUM7U0FDakg7SUFDRixDQUFDO0NBQ0Q7QUFFRCxTQUFTLHdCQUF3QjtJQUNoQyxJQUFJLEdBQVcsQ0FBQztJQUVoQixJQUFJO1FBQ0gsTUFBTSxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyx1RUFBdUUsRUFBRSxPQUFPLENBQUMsQ0FBQztRQUM5RyxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ2pDLEdBQUcsR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEdBQUcsQ0FBQztLQUNoQztJQUFDLE1BQU07UUFDUCxHQUFHLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQztLQUNiO0lBRUQsTUFBTSxPQUFPLEdBQUcsdUNBQXVDLENBQUM7SUFDeEQsTUFBTSxhQUFhLEdBQUcsSUFBSSxHQUFHLEVBQVUsQ0FBQztJQUV4QyxNQUFNLEtBQUssR0FBRyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDM0IsTUFBTSxNQUFNLEdBQUcsS0FBSztTQUNsQixJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQU8sRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztTQUNwRCxJQUFJLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQU8sRUFBRSxFQUFFO1FBQzVCLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ25DLE1BQU0sS0FBSyxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFFakMsSUFBSSxLQUFLLEVBQUU7WUFDVixhQUFhLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQzVCO0lBQ0YsQ0FBQyxFQUFFO1FBQ0YsTUFBTSxLQUFLLEdBQUcsQ0FBQyxHQUFHLGFBQWEsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO1FBQ2pELE1BQU0sUUFBUSxHQUFHO1lBQ2hCLGlHQUFpRztZQUNqRywrREFBK0Q7WUFDL0Qsa0dBQWtHO1lBQ2xHLGtHQUFrRztZQUNsRyxFQUFFO1lBQ0Ysb0RBQW9EO1lBQ3BELEVBQUU7WUFDRixnREFBZ0Q7WUFDaEQsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsS0FBSyxJQUFJLDZGQUE2RixJQUFJLFFBQVEsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLEdBQUcsRUFBRSxDQUFDLEVBQUU7WUFDMUosS0FBSztZQUNMLDZEQUE2RDtZQUM3RCxFQUFFO1NBQ0YsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUM7UUFFWixJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLElBQUksQ0FBQztZQUMxQixJQUFJLEVBQUUsbUVBQW1FO1lBQ3pFLFFBQVEsRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQztTQUMvQixDQUFDLENBQUMsQ0FBQztRQUNKLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDbEIsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUVMLE9BQU8sRUFBRSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUM7QUFDakMsQ0FBQztBQUVELE1BQU0sd0JBQXdCLEdBQUcsSUFBQSx5QkFBYyxFQUFDLG9CQUFvQixDQUFDLENBQUM7QUFFekQsUUFBQSwyQkFBMkIsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLDRCQUE0QixFQUFFLEdBQUcsRUFBRTtJQUN6RixPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsbUJBQW1CLENBQUM7U0FDbEMsSUFBSSxDQUFDLHdCQUF3QixFQUFFLENBQUM7U0FDaEMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7U0FDdEIsSUFBSSxDQUFDLHdCQUF3QixDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0FBQzVDLENBQUMsQ0FBQyxDQUFDO0FBRVUsUUFBQSx5QkFBeUIsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLDBCQUEwQixFQUFFLEdBQUcsRUFBRTtJQUNyRixNQUFNLElBQUksR0FBRyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLG1CQUFtQixDQUFDO1NBQzlDLElBQUksQ0FBQyx3QkFBd0IsRUFBRSxDQUFDO1NBQ2hDLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUUzQyxPQUFPLEtBQUssQ0FBQyxtQkFBbUIsRUFBRSxFQUFFLFNBQVMsRUFBRSxHQUFHLEVBQUUsQ0FBQztTQUNuRCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQztTQUN6QixJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO0FBQzFCLENBQUMsQ0FBQyxDQUFDIn0= \ No newline at end of file diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index d5da3f1cd89..cf2ab921f1c 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -93,9 +93,9 @@ function createCompile(src: string, build: boolean, emitError: boolean, transpil return pipeline; } -export function transpileTask(src: string, out: string, swc: boolean): () => NodeJS.ReadWriteStream { +export function transpileTask(src: string, out: string, swc: boolean): task.StreamTask { - return function () { + const task = () => { const transpile = createCompile(src, false, true, { swc }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); @@ -104,11 +104,14 @@ export function transpileTask(src: string, out: string, swc: boolean): () => Nod .pipe(transpile()) .pipe(gulp.dest(out)); }; + + task.taskName = `transpile-${path.basename(src)}`; + return task; } -export function compileTask(src: string, out: string, build: boolean, options: { disableMangle?: boolean } = {}): () => NodeJS.ReadWriteStream { +export function compileTask(src: string, out: string, build: boolean, options: { disableMangle?: boolean } = {}): task.StreamTask { - return function () { + const task = () => { if (os.totalmem() < 4_000_000_000) { throw new Error('compilation requires 4GB of RAM'); @@ -150,11 +153,14 @@ export function compileTask(src: string, out: string, build: boolean, options: { .pipe(compile()) .pipe(gulp.dest(out)); }; + + task.taskName = `compile-${path.basename(src)}`; + return task; } -export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteStream { +export function watchTask(out: string, build: boolean): task.StreamTask { - return function () { + const task = () => { const compile = createCompile('src', build, false, false); const src = gulp.src('src/**', { base: 'src' }); @@ -168,6 +174,8 @@ export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteSt .pipe(util.incremental(compile, src, true)) .pipe(gulp.dest(out)); }; + task.taskName = `watch-${path.basename(out)}`; + return task; } const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index f83225cc974..f7f31337124 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -58,6 +58,10 @@ "name": "vs/workbench/contrib/commands", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/mappedEdits", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/comments", "project": "vscode-workbench" diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 2af3addfa9f..a7d22a17b85 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -99,6 +99,7 @@ "--vscode-diffEditor-insertedTextBackground", "--vscode-diffEditor-insertedTextBorder", "--vscode-diffEditor-move-border", + "--vscode-diffEditor-moveActive-border", "--vscode-diffEditor-removedLineBackground", "--vscode-diffEditor-removedTextBackground", "--vscode-diffEditor-removedTextBorder", @@ -545,21 +546,31 @@ "--vscode-statusBar-noFolderBackground", "--vscode-statusBar-noFolderBorder", "--vscode-statusBar-noFolderForeground", - "--vscode-statusBar-offlineBackground", - "--vscode-statusBar-offlineForeground", + "--vscode-statusBarItem-offlineBackground", + "--vscode-statusBarItem-offlineForeground", + "--vscode-statusBarItem-offlineHoverBackground", + "--vscode-statusBarItem-offlineHoverForeground", "--vscode-statusBarItem-activeBackground", "--vscode-statusBarItem-compactHoverBackground", "--vscode-statusBarItem-errorBackground", "--vscode-statusBarItem-errorForeground", + "--vscode-statusBarItem-errorHoverBackground", + "--vscode-statusBarItem-errorHoverForeground", "--vscode-statusBarItem-focusBorder", "--vscode-statusBarItem-hoverBackground", + "--vscode-statusBarItem-hoverForeground", "--vscode-statusBarItem-prominentBackground", "--vscode-statusBarItem-prominentForeground", "--vscode-statusBarItem-prominentHoverBackground", + "--vscode-statusBarItem-prominentHoverForeground", "--vscode-statusBarItem-remoteBackground", "--vscode-statusBarItem-remoteForeground", + "--vscode-statusBarItem-remoteHoverBackground", + "--vscode-statusBarItem-remoteHoverForeground", "--vscode-statusBarItem-warningBackground", "--vscode-statusBarItem-warningForeground", + "--vscode-statusBarItem-warningHoverBackground", + "--vscode-statusBarItem-warningHoverForeground", "--vscode-symbolIcon-arrayForeground", "--vscode-symbolIcon-booleanForeground", "--vscode-symbolIcon-classForeground", @@ -768,4 +779,4 @@ "--z-index-notebook-sticky-scroll", "--zoom-factor" ] -} +} \ No newline at end of file diff --git a/cgmanifest.json b/cgmanifest.json index dfcdf4de6bb..574ec75d24d 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "8bab63006effd27646be3d5c07eb09d3282c9b4d" + "commitHash": "88c0a3606e1a856ad43651130d27bb880e1833d6" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "114.0.5735.248" + "version": "114.0.5735.289" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "d219fcc8b61dbeb459e92c90a069076f18a9a12a" + "commitHash": "34be316c404e84cdd967fa0e10fceeb6424eed90" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "25.4.0" + "version": "25.5.0" }, { "component": { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index f7c60045c12..a67cc7cf3bd 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -983,9 +983,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a25bee1f2ce..18f18069c1f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -37,7 +37,7 @@ libc = "0.2.144" tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "2621784a9ad72aa39500372391332a14bad581a3", default-features = false, features = ["connections"] } keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl"] } dialoguer = "0.10.4" -hyper = "0.14.26" +hyper = { version = "0.14.26", features = ["server", "http1", "runtime"] } indicatif = "0.17.4" tempfile = "3.5.0" clap_lex = "0.5.0" diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index 8c32ee14d89..b104976b9ab 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -8,7 +8,7 @@ use std::process::Command; use clap::Parser; use cli::{ - commands::{args, tunnels, update, version, CommandContext}, + commands::{args, serve_web, tunnels, update, version, CommandContext}, constants::get_default_user_agent, desktop, log, state::LauncherPaths, @@ -99,6 +99,10 @@ async fn main() -> Result<(), std::convert::Infallible> { tunnels::command_shell(context!(), cs_args).await } + Some(args::Commands::ServeWeb(sw_args)) => { + serve_web::serve_web(context!(), sw_args).await + } + Some(args::Commands::Tunnel(tunnel_args)) => match tunnel_args.subcommand { Some(args::TunnelSubcommand::Prune) => tunnels::prune(context!()).await, Some(args::TunnelSubcommand::Unregister) => tunnels::unregister(context!()).await, diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 754729f2c04..d10a52ad774 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -9,4 +9,5 @@ pub mod args; pub mod tunnels; pub mod update; pub mod version; +pub mod serve_web; pub use context::CommandContext; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 9caee09ed64..cce01c52fd9 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -172,11 +172,42 @@ pub enum Commands { /// Changes the version of the editor you're using. Version(VersionArgs), + /// Runs a local web version of VS Code. + ServeWeb(ServeWebArgs), + /// Runs the control server on process stdin/stdout #[clap(hide = true)] CommandShell(CommandShellArgs), } +#[derive(Args, Debug, Clone)] +pub struct ServeWebArgs { + /// Host to listen on, defaults to 'localhost' + #[clap(long)] + pub host: Option, + /// Port to listen on. If 0 is passed a random free port is picked. + #[clap(long, default_value_t = 8000)] + pub port: u16, + /// A secret that must be included with all requests. + #[clap(long)] + pub connection_token: Option, + /// Run without a connection token. Only use this if the connection is secured by other means. + #[clap(long)] + pub without_connection_token: bool, + /// If set, the user accepts the server license terms and the server will be started without a user prompt. + #[clap(long)] + pub accept_server_license_terms: bool, + /// Specifies the directory that server data is kept in. + #[clap(long)] + pub server_data_dir: Option, + /// Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code. + #[clap(long)] + pub user_data_dir: Option, + /// Set the root path for extensions. + #[clap(long)] + pub extensions_dir: Option, +} + #[derive(Args, Debug, Clone)] pub struct CommandShellArgs { /// Listen on a socket instead of stdin/stdout. diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs new file mode 100644 index 00000000000..b2bf4d431e4 --- /dev/null +++ b/cli/src/commands/serve_web.rs @@ -0,0 +1,617 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use std::collections::HashMap; +use std::convert::Infallible; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request, Response, Server}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::pin; +use tokio::process::Command; + +use crate::async_pipe::{get_socket_name, get_socket_rw_stream, AsyncPipe}; +use crate::constants::VSCODE_CLI_QUALITY; +use crate::download_cache::DownloadCache; +use crate::log; +use crate::options::Quality; +use crate::update_service::{ + unzip_downloaded_release, Platform, Release, TargetKind, UpdateService, +}; +use crate::util::errors::AnyError; +use crate::util::http::{self, ReqwestSimpleHttp}; +use crate::util::io::SilentCopyProgress; +use crate::util::sync::{new_barrier, Barrier, BarrierOpener}; +use crate::{ + tunnels::legal, + util::{errors::CodeError, prereqs::PreReqChecker}, +}; + +use super::{args::ServeWebArgs, CommandContext}; + +/// Length of a commit hash, for validation +const COMMIT_HASH_LEN: usize = 40; +/// Number of seconds where, if there's no connections to a VS Code server, +/// the server is shut down. +const SERVER_IDLE_TIMEOUT_SECS: u64 = 60 * 60; +/// Number of seconds in which the server times out when there is a connection +/// (should be large enough to basically never happen) +const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12; +/// How long to cache the "latest" version we get from the update service. +const RELEASE_CACHE_SECS: u64 = 60 * 60; + +/// Implements the vscode "server of servers". Clients who go to the URI get +/// served the latest version of the VS Code server whenever they load the +/// page. The VS Code server prefixes all assets and connections it loads with +/// its version string, so existing clients can continue to get served even +/// while new clients get new VS Code Server versions. +pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result { + legal::require_consent(&ctx.paths, args.accept_server_license_terms)?; + let mut addr: SocketAddr = match &args.host { + Some(h) => h.parse().map_err(CodeError::InvalidHostAddress)?, + None => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + }; + addr.set_port(args.port); + + let platform: crate::update_service::Platform = PreReqChecker::new().verify().await?; + + if !args.without_connection_token { + // Ensure there's a defined connection token, since if multiple server versions + // are excuted, they will need to have a single shared token. + let connection_token = args + .connection_token + .clone() + .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + ctx.log.result(format!( + "Web UI available at http://{}?tkn={}", + addr, connection_token, + )); + args.connection_token = Some(connection_token); + } else { + ctx.log + .result(format!("Web UI available at http://{}", addr)); + args.connection_token = None; + } + + let cm = ConnectionManager::new(&ctx, platform, args); + let make_svc = make_service_fn(move |_conn| { + let cm = cm.clone(); + let log = ctx.log.clone(); + let service = service_fn(move |req| handle(cm.clone(), log.clone(), req)); + async move { Ok::<_, Infallible>(service) } + }); + + let server = Server::bind(&addr).serve(make_svc); + + server.await.map_err(CodeError::CouldNotListenOnInterface)?; + + Ok(0) +} + +/// Handler function for an inbound request +async fn handle( + cm: Arc, + log: log::Logger, + req: Request, +) -> Result, Infallible> { + let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), cm.platform) { + r + } else { + match cm.get_latest_release().await { + Ok(r) => r, + Err(e) => { + error!(log, "error getting latest version: {}", e); + return Ok(response::code_err(e)); + } + } + }; + + Ok(match cm.get_connection(release).await { + Ok(rw) => { + if req.headers().contains_key(hyper::header::UPGRADE) { + forward_ws_req_to_server(cm.log.clone(), rw, req).await + } else { + forward_http_req_to_server(rw, req).await + } + } + Err(CodeError::ServerNotYetDownloaded) => response::wait_for_download(), + Err(e) => response::code_err(e), + }) +} + +/// Gets the release info from the VS Code path prefix, which is in the +/// format `/-/...` +fn get_release_from_path(path: &str, platform: Platform) -> Option<(Release, String)> { + if !path.starts_with('/') { + return None; // paths must start with '/' + } + + let path = &path[1..]; + let i = path.find('/').unwrap_or(path.len()); + let quality_commit_sep = path.get(..i).and_then(|p| p.find('-'))?; + + let (quality_commit, remaining) = path.split_at(i); + let (quality, commit) = quality_commit.split_at(quality_commit_sep); + + if !is_commit_hash(commit) { + return None; + } + + Some(( + Release { + // remember to trim off the leading '/' which is now part of th quality + quality: Quality::try_from(quality).ok()?, + commit: commit.to_string(), + platform, + target: TargetKind::Web, + name: "".to_string(), + }, + remaining.to_string(), + )) +} + +/// Proxies the standard HTTP request to the async pipe, returning the piped response +async fn forward_http_req_to_server( + (rw, handle): (AsyncPipe, ConnectionHandle), + req: Request, +) -> Response { + let (mut request_sender, connection) = + match hyper::client::conn::Builder::new().handshake(rw).await { + Ok(r) => r, + Err(e) => return response::connection_err(e), + }; + + tokio::spawn(connection); + + let res = request_sender + .send_request(req) + .await + .unwrap_or_else(response::connection_err); + + // technically, we should buffer the body into memory since it may not be + // read at this point, but because the keepalive time is very large + // there's not going to be responses that take hours to send and x + // cause us to kill the server before the response is sent + drop(handle); + + res +} + +/// Proxies the websocket request to the async pipe +async fn forward_ws_req_to_server( + log: log::Logger, + (rw, handle): (AsyncPipe, ConnectionHandle), + mut req: Request, +) -> Response { + // splicing of client and servers inspired by https://github.com/hyperium/hyper/blob/fece9f7f50431cf9533cfe7106b53a77b48db699/examples/upgrades.rs + let (mut request_sender, connection) = + match hyper::client::conn::Builder::new().handshake(rw).await { + Ok(r) => r, + Err(e) => return response::connection_err(e), + }; + + tokio::spawn(connection); + + let mut proxied_req = Request::builder().uri(req.uri()); + for (k, v) in req.headers() { + proxied_req = proxied_req.header(k, v); + } + + let mut res = request_sender + .send_request(proxied_req.body(Body::empty()).unwrap()) + .await + .unwrap_or_else(response::connection_err); + + let mut proxied_res = Response::new(Body::empty()); + *proxied_res.status_mut() = res.status(); + for (k, v) in res.headers() { + proxied_res.headers_mut().insert(k, v.clone()); + } + + // only start upgrade at this point in case the server decides to deny socket + if res.status() == hyper::StatusCode::SWITCHING_PROTOCOLS { + tokio::spawn(async move { + let (s_req, s_res) = + tokio::join!(hyper::upgrade::on(&mut req), hyper::upgrade::on(&mut res)); + + match (s_req, s_res) { + (Err(e1), Err(e2)) => debug!( + log, + "client ({}) and server ({}) websocket upgrade failed", e1, e2 + ), + (Err(e1), _) => debug!(log, "client ({}) websocket upgrade failed", e1), + (_, Err(e2)) => debug!(log, "server ({}) websocket upgrade failed", e2), + (Ok(mut s_req), Ok(mut s_res)) => { + trace!(log, "websocket upgrade succeeded"); + let r = tokio::io::copy_bidirectional(&mut s_req, &mut s_res).await; + trace!(log, "websocket closed (error: {:?})", r.err()); + } + } + + drop(handle); + }); + } + + proxied_res +} + +/// Returns whether the string looks like a commit hash. +fn is_commit_hash(s: &str) -> bool { + s.len() == COMMIT_HASH_LEN && s.chars().all(|c| c.is_ascii_hexdigit()) +} + +/// Module holding original responses the CLI's server makes. +mod response { + use const_format::concatcp; + + use crate::constants::QUALITYLESS_SERVER_NAME; + + use super::*; + + pub fn connection_err(err: hyper::Error) -> Response { + Response::builder() + .status(503) + .body(Body::from(format!("Error connecting to server: {:?}", err))) + .unwrap() + } + + pub fn code_err(err: CodeError) -> Response { + Response::builder() + .status(500) + .body(Body::from(format!("Error serving request: {}", err))) + .unwrap() + } + + pub fn wait_for_download() -> Response { + Response::builder() + .status(202) + .header("Content-Type", "text/html") // todo: get latest + .body(Body::from(concatcp!("The latest version of the ", QUALITYLESS_SERVER_NAME, " is downloading, please wait a moment...", ))) + .unwrap() + } +} + +/// Handle returned when getting a stream to the server, used to refcount +/// connections to a server so it can be disposed when there are no more clients. +struct ConnectionHandle { + client_counter: Arc>, +} + +impl ConnectionHandle { + pub fn new(client_counter: Arc>) -> Self { + client_counter.send_modify(|v| { + *v += 1; + }); + Self { client_counter } + } +} + +impl Drop for ConnectionHandle { + fn drop(&mut self) { + self.client_counter.send_modify(|v| { + *v -= 1; + }); + } +} + +type StartData = (PathBuf, Arc>); + +/// State stored in the ConnectionManager for each server version. +struct VersionState { + downloaded: bool, + socket_path: Barrier>, +} + +type ConnectionStateMap = Arc>>; + +/// Manages the connections to running web UI instances. Multiple web servers +/// can run concurrently, with routing based on the URL path. +struct ConnectionManager { + pub platform: Platform, + pub log: log::Logger, + args: ServeWebArgs, + /// Cache where servers are stored + cache: DownloadCache, + /// Mapping of (Quality, Commit) to the state each server is in + state: ConnectionStateMap, + /// Update service instance + update_service: UpdateService, + /// Cache of the latest released version, storing the time we checked as well + latest_version: tokio::sync::Mutex>, +} + +fn key_for_release(release: &Release) -> (Quality, String) { + (release.quality, release.commit.clone()) +} + +impl ConnectionManager { + pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { + Arc::new(Self { + platform, + args, + log: ctx.log.clone(), + cache: DownloadCache::new(ctx.paths.web_server_storage()), + update_service: UpdateService::new( + ctx.log.clone(), + Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), + ), + state: ConnectionStateMap::default(), + latest_version: tokio::sync::Mutex::default(), + }) + } + + /// Gets a connection to a server version + pub async fn get_connection( + &self, + release: Release, + ) -> Result<(AsyncPipe, ConnectionHandle), CodeError> { + // todo@connor4312: there is likely some performance benefit to + // implementing a 'keepalive' for these connections. + let (path, counter) = self.get_version_data(release).await?; + let handle = ConnectionHandle::new(counter); + let rw = get_socket_rw_stream(&path).await?; + Ok((rw, handle)) + } + + /// Gets the latest release for the CLI quality, caching its result for some + /// time to allow for fast loads. + pub async fn get_latest_release(&self) -> Result { + let mut latest = self.latest_version.lock().await; + let now = Instant::now(); + if let Some((checked_at, release)) = &*latest { + if checked_at.elapsed() < Duration::from_secs(RELEASE_CACHE_SECS) { + return Ok(release.clone()); + } + } + + let quality = VSCODE_CLI_QUALITY + .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .and_then(|q| { + Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) + })?; + + let release = self + .update_service + .get_latest_commit(self.platform, TargetKind::Web, quality) + .await + .map_err(|e| CodeError::UpdateCheckFailed(e.to_string())); + + // If the update service is unavailable and we have stale data, use that + if let (Err(e), Some((_, previous))) = (&release, &*latest) { + warning!(self.log, "error getting latest release, using stale: {}", e); + return Ok(previous.clone()); + } + + let release = release?; + debug!(self.log, "refreshed latest release: {}", release); + *latest = Some((now, release.clone())); + + Ok(release) + } + + /// Gets the StartData for the a version of the VS Code server, triggering + /// download/start if necessary. It returns `CodeError::ServerNotYetDownloaded` + /// while the server is downloading, which is used to have a refresh loop on the page. + async fn get_version_data(&self, release: Release) -> Result { + self.get_version_data_inner(release)? + .wait() + .await + .unwrap() + .map_err(CodeError::ServerDownloadError) + } + + fn get_version_data_inner( + &self, + release: Release, + ) -> Result>, CodeError> { + let mut state = self.state.lock().unwrap(); + let key = key_for_release(&release); + if let Some(s) = state.get_mut(&key) { + if !s.downloaded { + if s.socket_path.is_open() { + s.downloaded = true; + } else { + return Err(CodeError::ServerNotYetDownloaded); + } + } + + return Ok(s.socket_path.clone()); + } + + let (socket_path, opener) = new_barrier(); + let state_map_dup = self.state.clone(); + let args = StartArgs { + args: self.args.clone(), + log: self.log.clone(), + opener, + release, + }; + + if let Some(p) = self.cache.exists(&args.release.commit) { + state.insert( + key.clone(), + VersionState { + socket_path: socket_path.clone(), + downloaded: true, + }, + ); + + tokio::spawn(async move { + Self::start_version(args, p).await; + state_map_dup.lock().unwrap().remove(&key); + }); + Ok(socket_path) + } else { + state.insert( + key.clone(), + VersionState { + socket_path, + downloaded: false, + }, + ); + let update_service = self.update_service.clone(); + let cache = self.cache.clone(); + tokio::spawn(async move { + Self::download_version(args, update_service.clone(), cache.clone()).await; + state_map_dup.lock().unwrap().remove(&key); + }); + Err(CodeError::ServerNotYetDownloaded) + } + } + + /// Downloads a server version into the cache and starts it. + async fn download_version( + args: StartArgs, + update_service: UpdateService, + cache: DownloadCache, + ) { + let release_for_fut = args.release.clone(); + let log_for_fut = args.log.clone(); + let dir_fut = cache.create(&args.release.commit, |target_dir| async move { + info!(log_for_fut, "Downloading server {}", release_for_fut.commit); + let tmpdir = tempfile::tempdir().unwrap(); + let response = update_service.get_download_stream(&release_for_fut).await?; + + let name = response.url_path_basename().unwrap(); + let archive_path = tmpdir.path().join(name); + http::download_into_file( + &archive_path, + log_for_fut.get_download_logger("Downloading server:"), + response, + ) + .await?; + unzip_downloaded_release(&archive_path, &target_dir, SilentCopyProgress())?; + Ok(()) + }); + + match dir_fut.await { + Err(e) => args.opener.open(Err(e.to_string())), + Ok(dir) => Self::start_version(args, dir).await, + } + } + + /// Starts a downloaded server that can be found in the given `path`. + async fn start_version(args: StartArgs, path: PathBuf) { + info!(args.log, "Starting server {}", args.release.commit); + + let executable = path + .join("bin") + .join(args.release.quality.server_entrypoint()); + let socket_path = get_socket_name(); + + #[cfg(not(windows))] + let mut cmd = Command::new(&executable); + #[cfg(windows)] + let mut cmd = { + let mut cmd = Command::new("cmd"); + cmd.arg("/Q"); + cmd.arg("/C"); + cmd.arg(&executable); + cmd + }; + + cmd.stdin(std::process::Stdio::null()); + cmd.stderr(std::process::Stdio::piped()); + cmd.stdout(std::process::Stdio::piped()); + cmd.arg("--socket-path"); + cmd.arg(&socket_path); + + // License agreement already checked by the `server_web` function. + cmd.args(["--accept-server-license-terms"]); + + if let Some(a) = &args.args.server_data_dir { + cmd.arg("--server-data-dir"); + cmd.arg(a); + } + if let Some(a) = &args.args.user_data_dir { + cmd.arg("--user-data-dir"); + cmd.arg(a); + } + if let Some(a) = &args.args.extensions_dir { + cmd.arg("--extensions-dir"); + cmd.arg(a); + } + if args.args.without_connection_token { + cmd.arg("--without-connection-token"); + } + if let Some(ct) = &args.args.connection_token { + cmd.arg("--connection-token"); + cmd.arg(ct); + } + + // removed, otherwise the workbench will not be usable when running the CLI from sources. + cmd.env_remove("VSCODE_DEV"); + + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + args.opener.open(Err(e.to_string())); + return; + } + }; + + let (mut stdout, mut stderr) = ( + BufReader::new(child.stdout.take().unwrap()).lines(), + BufReader::new(child.stderr.take().unwrap()).lines(), + ); + + // wrapped option to prove that we only use this once in the loop + let (counter_tx, mut counter_rx) = tokio::sync::watch::channel(0); + let mut opener = Some((args.opener, socket_path, Arc::new(counter_tx))); + let commit_prefix = &args.release.commit[..7]; + let kill_timer = tokio::time::sleep(Duration::from_secs(SERVER_IDLE_TIMEOUT_SECS)); + pin!(kill_timer); + + loop { + tokio::select! { + Ok(Some(l)) = stdout.next_line() => { + info!(args.log, "[{} stdout]: {}", commit_prefix, l); + + if l.contains("Server bound to") { + if let Some((opener, path, counter_tx)) = opener.take() { + opener.open(Ok((path, counter_tx))); + } + } + } + Ok(Some(l)) = stderr.next_line() => { + info!(args.log, "[{} stderr]: {}", commit_prefix, l); + }, + n = counter_rx.changed() => { + kill_timer.as_mut().reset(match n { + // err means that the record was dropped + Err(_) => tokio::time::Instant::now(), + Ok(_) => { + if *counter_rx.borrow() == 0 { + tokio::time::Instant::now() + Duration::from_secs(SERVER_IDLE_TIMEOUT_SECS) + } else { + tokio::time::Instant::now() + Duration::from_secs(SERVER_ACTIVE_TIMEOUT_SECS) + } + } + }); + } + _ = &mut kill_timer => { + info!(args.log, "[{} process]: idle timeout reached, ending", commit_prefix); + let _ = child.kill().await; + break; + } + e = child.wait() => { + info!(args.log, "[{} process]: exited: {:?}", commit_prefix, e); + break; + } + } + } + } +} + +struct StartArgs { + log: log::Logger, + args: ServeWebArgs, + release: Release, + opener: BarrierOpener>, +} diff --git a/cli/src/self_update.rs b/cli/src/self_update.rs index 2e95719a3b9..4a878dc5447 100644 --- a/cli/src/self_update.rs +++ b/cli/src/self_update.rs @@ -11,7 +11,7 @@ use crate::{ options::Quality, update_service::{unzip_downloaded_release, Platform, Release, TargetKind, UpdateService}, util::{ - errors::{wrap, AnyError, CorruptDownload, UpdatesNotConfigured}, + errors::{wrap, AnyError, CodeError, CorruptDownload}, http, io::{ReportCopyProgress, SilentCopyProgress}, }, @@ -27,14 +27,16 @@ pub struct SelfUpdate<'a> { impl<'a> SelfUpdate<'a> { pub fn new(update_service: &'a UpdateService) -> Result { let commit = VSCODE_CLI_COMMIT - .ok_or_else(|| UpdatesNotConfigured("unknown build commit".to_string()))?; + .ok_or_else(|| CodeError::UpdatesNotConfigured("unknown build commit"))?; let quality = VSCODE_CLI_QUALITY - .ok_or_else(|| UpdatesNotConfigured("no configured quality".to_string())) - .and_then(|q| Quality::try_from(q).map_err(UpdatesNotConfigured))?; + .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .and_then(|q| { + Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) + })?; let platform = Platform::env_default().ok_or_else(|| { - UpdatesNotConfigured("Unknown platform, please report this error".to_string()) + CodeError::UpdatesNotConfigured("Unknown platform, please report this error") })?; Ok(Self { diff --git a/cli/src/state.rs b/cli/src/state.rs index 1b1ff343da5..8815e2df40c 100644 --- a/cli/src/state.rs +++ b/cli/src/state.rs @@ -212,4 +212,10 @@ impl LauncherPaths { ) }) } + + /// Suggested path for web server storage + pub fn web_server_storage(&self) -> PathBuf { + self.root.join("serve-web") + } + } diff --git a/cli/src/tunnels/legal.rs b/cli/src/tunnels/legal.rs index 676ccb7da55..35316af4fde 100644 --- a/cli/src/tunnels/legal.rs +++ b/cli/src/tunnels/legal.rs @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use crate::constants::{IS_INTERACTIVE_CLI, PRODUCT_NAME_LONG}; +use crate::constants::IS_INTERACTIVE_CLI; use crate::state::{LauncherPaths, PersistedState}; -use crate::util::errors::{AnyError, MissingLegalConsent}; +use crate::util::errors::{AnyError, CodeError}; use crate::util::input::prompt_yn; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; @@ -46,23 +46,14 @@ pub fn require_consent( if accept_server_license_terms { load.consented = Some(true); } else if !*IS_INTERACTIVE_CLI { - return Err(MissingLegalConsent( - "Run this command again with --accept-server-license-terms to indicate your agreement." - .to_string(), - ) - .into()); + return Err(CodeError::NeedsInteractiveLegalConsent.into()); } else { match prompt_yn(prompt) { Ok(true) => { load.consented = Some(true); } - Ok(false) => { - return Err(AnyError::from(MissingLegalConsent(format!( - "Sorry you cannot use {} CLI without accepting the terms.", - PRODUCT_NAME_LONG - )))) - } - Err(e) => return Err(AnyError::from(MissingLegalConsent(e.to_string()))), + Ok(false) => return Err(CodeError::DeniedLegalConset.into()), + Err(_) => return Err(CodeError::NeedsInteractiveLegalConsent.into()), } } diff --git a/cli/src/update_service.rs b/cli/src/update_service.rs index b03d8ea5963..d218e4a1333 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -11,7 +11,7 @@ use crate::{ constants::VSCODE_CLI_UPDATE_ENDPOINT, debug, log, options, spanf, util::{ - errors::{AnyError, CodeError, UpdatesNotConfigured, WrappedError}, + errors::{AnyError, CodeError, WrappedError}, http::{BoxedHttp, SimpleResponse}, io::ReportCopyProgress, tar, zipper, @@ -19,6 +19,7 @@ use crate::{ }; /// Implementation of the VS Code Update service for use in the CLI. +#[derive(Clone)] pub struct UpdateService { client: BoxedHttp, log: log::Logger, @@ -54,6 +55,10 @@ fn quality_download_segment(quality: options::Quality) -> &'static str { } } +fn get_update_endpoint() -> Result<&'static str, CodeError> { + VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(|| CodeError::UpdatesNotConfigured("no service url")) +} + impl UpdateService { pub fn new(log: log::Logger, http: BoxedHttp) -> Self { UpdateService { client: http, log } @@ -66,8 +71,7 @@ impl UpdateService { quality: options::Quality, version: &str, ) -> Result { - let update_endpoint = - VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(UpdatesNotConfigured::no_url)?; + let update_endpoint = get_update_endpoint()?; let download_segment = target .download_segment(platform) .ok_or_else(|| CodeError::UnsupportedPlatform(platform.to_string()))?; @@ -108,8 +112,7 @@ impl UpdateService { target: TargetKind, quality: options::Quality, ) -> Result { - let update_endpoint = - VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(UpdatesNotConfigured::no_url)?; + let update_endpoint = get_update_endpoint()?; let download_segment = target .download_segment(platform) .ok_or_else(|| CodeError::UnsupportedPlatform(platform.to_string()))?; @@ -144,8 +147,7 @@ impl UpdateService { /// Gets the download stream for the release. pub async fn get_download_stream(&self, release: &Release) -> Result { - let update_endpoint = - VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(UpdatesNotConfigured::no_url)?; + let update_endpoint = get_update_endpoint()?; let download_segment = release .target .download_segment(release.platform) diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index c82e14acc8b..38d9b36f54b 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -108,16 +108,6 @@ impl StatusError { } } -// When the user has not consented to the licensing terms in using the Launcher -#[derive(Debug)] -pub struct MissingLegalConsent(pub String); - -impl std::fmt::Display for MissingLegalConsent { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - // When the provided connection token doesn't match the one used to set up the original VS Code Server // This is most likely due to a new user joining. #[derive(Debug)] @@ -313,20 +303,6 @@ impl std::fmt::Display for ServerHasClosed { } } -#[derive(Debug)] -pub struct UpdatesNotConfigured(pub String); - -impl UpdatesNotConfigured { - pub fn no_url() -> Self { - UpdatesNotConfigured("no service url".to_owned()) - } -} - -impl std::fmt::Display for UpdatesNotConfigured { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Update service is not configured: {}", self.0) - } -} #[derive(Debug)] pub struct ServiceAlreadyRegistered(); @@ -517,10 +493,28 @@ pub enum CodeError { KeyringTimeout, #[error("no host is connected to the tunnel relay")] NoTunnelEndpoint, + #[error("could not parse `host`: {0}")] + InvalidHostAddress(std::net::AddrParseError), + #[error("could not start server on the given host/port: {0}")] + CouldNotListenOnInterface(hyper::Error), + #[error( + "Run this command again with --accept-server-license-terms to indicate your agreement." + )] + NeedsInteractiveLegalConsent, + #[error("Sorry, you cannot use this CLI without accepting the terms.")] + DeniedLegalConset, + #[error("The server is not yet downloaded, try again shortly.")] + ServerNotYetDownloaded, + #[error("An error was encountered downloading the server, please retry: {0}")] + ServerDownloadError(String), + #[error("Updates are are not available: {0}")] + UpdatesNotConfigured(&'static str), + // todo: can be specialized when update service is moved to CodeErrors + #[error("Could not check for update: {0}")] + UpdateCheckFailed(String), } makeAnyError!( - MissingLegalConsent, MismatchConnectionToken, DevTunnelError, StatusError, @@ -543,7 +537,6 @@ makeAnyError!( ServerHasClosed, ServiceAlreadyRegistered, WindowsNeedsElevation, - UpdatesNotConfigured, CorruptDownload, MissingHomeDirectory, OAuthError, diff --git a/cli/src/util/sync.rs b/cli/src/util/sync.rs index 8b653cd2d53..67c777b75ed 100644 --- a/cli/src/util/sync.rs +++ b/cli/src/util/sync.rs @@ -63,7 +63,7 @@ impl BarrierOpener { /// and is thereafter permanently closed. It can contain a value. pub fn new_barrier() -> (Barrier, BarrierOpener) where - T: Copy, + T: Clone, { let (closed_tx, closed_rx) = watch::channel(None); (Barrier(closed_rx), BarrierOpener(Arc::new(closed_tx))) diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index 6d9d794265b..5e5bd571d69 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -24,7 +24,7 @@ }, { "type": "string", - "pattern": "^([a-z0-9-]+):(\\d{1,5})$" + "pattern": "^([a-z0-9_-]+):(\\d{1,5})$" } ] } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index ab03a354d11..62bdf24ebcb 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -481,7 +481,7 @@ export class Git { const result = await this.exec(pathInsidePossibleRepository, ['rev-parse', '--show-toplevel']); // Keep trailing spaces which are part of the directory name - const repositoryRootPath = path.normalize(result.stdout.trimLeft().replace(/[\r\n]+$/, '')); + const repositoryRootPath = path.normalize(result.stdout.trimStart().replace(/[\r\n]+$/, '')); if (isWindows) { // On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped @@ -528,7 +528,7 @@ export class Git { !isDescendant(pathInsidePossibleRepository, repositoryRootPath) && this.compareGitVersionTo('2.31.0') !== -1) { const relativePathResult = await this.exec(pathInsidePossibleRepository, ['rev-parse', '--path-format=relative', '--show-toplevel',]); - return path.resolve(pathInsidePossibleRepository, relativePathResult.stdout.trimLeft().replace(/[\r\n]+$/, '')); + return path.resolve(pathInsidePossibleRepository, relativePathResult.stdout.trimStart().replace(/[\r\n]+$/, '')); } return repositoryRootPath; diff --git a/extensions/html-language-features/.vscode/settings.json b/extensions/html-language-features/.vscode/settings.json index 569ac10cf8f..17b02728e8c 100644 --- a/extensions/html-language-features/.vscode/settings.json +++ b/extensions/html-language-features/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.insertSpaces": false, "prettier.semi": true, - "prettier.singleQuote": true, - "prettier.printWidth": 120, -} \ No newline at end of file + "prettier.singleQuote": true, + "prettier.printWidth": 120 +} diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index ce667a5d277..f5e25ac3695 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -56,7 +56,11 @@ { "command": "ipynb.cleanInvalidImageAttachment", "title": "%cleanInvalidImageAttachment.title%" - } + }, + { + "command": "notebook.cellOutput.copyToClipboard", + "title": "%copyOutputToClipboard.title%" + } ], "notebooks": [ { @@ -99,7 +103,13 @@ "command": "ipynb.cleanInvalidImageAttachment", "when": "false" } - ] + ], + "webview/context": [ + { + "command": "notebook.cellOutput.copyToClipboard", + "when": "webviewId == 'notebook.output' && webviewSection == 'image'" + } + ] } }, "scripts": { diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index bd8e0ab1da0..45aa2aa03e8 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -6,6 +6,7 @@ "newUntitledIpynb.shortTitle": "Jupyter Notebook", "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", "cleanInvalidImageAttachment.title": "Clean Invalid Image Attachment Reference", + "copyOutputToClipboard.title": "Copy Output to Clipboard", "markdownAttachmentRenderer.displayName": { "message": "Markdown-It ipynb Cell Attachment renderer", "comment": [ diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 10895276e1f..457a40f6a74 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext, OutputChannel, window, workspace, l10n } from 'vscode'; +import { ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription } from '../jsonClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; @@ -129,7 +129,10 @@ async function getSchemaRequestService(context: ExtensionContext, log: Log): Pro const isXHRResponse = (error: any): error is XHRResponse => typeof error?.status === 'number'; const request = async (uri: string, etag?: string): Promise => { - const headers: Headers = { 'Accept-Encoding': 'gzip, deflate' }; + const headers: Headers = { + 'Accept-Encoding': 'gzip, deflate', + 'User-Agent': `${env.appName} (${env.appHost})` + }; if (etag) { headers['If-None-Match'] = etag; } diff --git a/extensions/json-language-features/package.nls.json b/extensions/json-language-features/package.nls.json index 4091dad211a..df68b3f8eac 100644 --- a/extensions/json-language-features/package.nls.json +++ b/extensions/json-language-features/package.nls.json @@ -2,9 +2,9 @@ "displayName": "JSON Language Features", "description": "Provides rich language support for JSON files.", "json.schemas.desc": "Associate schemas to JSON files in the current project.", - "json.schemas.url.desc": "A URL to a schema or a relative path to a schema in the current directory", - "json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas. `*` can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern.", - "json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' to match against when resolving JSON files to schemas.", + "json.schemas.url.desc": "A URL or absolute file path to a schema. Can be a relative path in workspace and workspace folder settings.", + "json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas. `*` and '**' can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern.", + "json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' and '**' to match against when resolving JSON files to schemas. When beginning with '!', it defines an exclusion pattern.", "json.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.", "json.format.enable.desc": "Enable/disable default JSON formatter", "json.format.keepLines.desc" : "Keep all existing new lines when formatting.", diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 939eafc442b..dba6176ffda 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -106,10 +106,10 @@ sup { line-height: 0; } -ul ul, -ul ol, -ol ul, -ol ol { +ul ul:first-child, +ul ol:first-child, +ol ul:first-child, +ol ol:first-child { margin-bottom: 0; } @@ -138,6 +138,10 @@ p { margin-bottom: 16px; } +li p { + margin-bottom: 0.7em; +} + ul, ol { margin-bottom: 0.7em; diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 60e80b169d3..78c71ce9138 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -123,7 +123,8 @@ "commands": [ { "command": "_markdown.copyImage", - "title": "%markdown.copyImage.title%" + "title": "%markdown.copyImage.title%", + "category": "Markdown" }, { "command": "markdown.showPreview", @@ -244,6 +245,10 @@ } ], "commandPalette": [ + { + "command": "_markdown.copyImage", + "when": "false" + }, { "command": "markdown.showPreview", "when": "editorLangId == markdown && !notebookEditorFocused", diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 090e9719420..c5b4a631afc 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -37,6 +37,7 @@ function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable if (alt) { image.alt = alt; } + image.setAttribute('data-vscode-context', JSON.stringify({ webviewSection: 'image', outputId: outputInfo.id, 'preventDefaultContextMenuItems': true })); const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); diff --git a/extensions/package.json b/extensions/package.json index b0bac44993d..02a1f822e7d 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.2.0-dev.20230807" + "typescript": "^5.2.1-rc" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 5ee95741ff0..9d50e393547 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -19,6 +19,7 @@ "fileSearchProvider", "findTextInFiles", "fsChunks", + "mappedEditsProvider", "notebookCellExecutionState", "notebookDeprecated", "notebookLiveShare", @@ -45,7 +46,7 @@ "timeline", "tokenInformation", "treeItemCheckbox", - "treeViewActiveItem", + "treeViewActiveItem", "treeViewReveal", "workspaceTrust", "telemetry", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts new file mode 100644 index 00000000000..f4fc349f115 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as assert from 'assert'; + +suite('mapped edits provider', () => { + + test('mapped edits does not provide edits for unregistered langs', async function () { + + const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts')); + + const tsDocFilter = [{ language: 'json' }]; + + const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, { + provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => { + + assert(context.selections.length === 1); + assert(context.related.length === 1); + assert('uri' in context.related[0] && 'range' in context.related[0]); + + const edit = new vscode.WorkspaceEdit(); + const text = codeBlocks.join('\n//----\n'); + edit.replace(uri, context.selections[0], text); + return edit; + } + }); + await vscode.workspace.openTextDocument(uri); + const result = await vscode.commands.executeCommand>( + 'vscode.executeMappedEditsProvider', + uri, + [ + '// hello', + `function foo() {\n\treturn 1;\n}`, + ], + { + selections: [new vscode.Selection(0, 0, 1, 0)], + related: [ + { + uri, + range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0)) + } + ] + } + ); + r1.dispose(); + + assert(result === null, 'returned null'); + }); + + test('mapped edits provides a single edit replacing the selection', async function () { + + const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts')); + + const tsDocFilter = [{ language: 'typescript' }]; + + const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, { + provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => { + + assert(context.selections.length === 1); + assert(context.related.length === 1); + assert('uri' in context.related[0] && 'range' in context.related[0]); + + const edit = new vscode.WorkspaceEdit(); + const text = codeBlocks.join('\n//----\n'); + edit.replace(uri, context.selections[0], text); + return edit; + } + }); + + await vscode.workspace.openTextDocument(uri); + const result = await vscode.commands.executeCommand>( + 'vscode.executeMappedEditsProvider', + uri, + [ + '// hello', + `function foo() {\n\treturn 1;\n}`, + ], + { + selections: [new vscode.Selection(0, 0, 1, 0)], + related: [ + { + uri, + range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0)) + } + ] + } + ); + r1.dispose(); + + assert(result, 'non null response'); + const edits = result.get(uri); + assert(edits.length === 1); + assert(edits[0].range.start.line === 0); + assert(edits[0].range.start.character === 0); + assert(edits[0].range.end.line === 1); + assert(edits[0].range.end.character === 0); + }); +}); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index f8c8b806853..76408d2de77 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, doesNotThrow, equal, ok, strictEqual, throws } from 'assert'; -import { commands, ConfigurationTarget, Disposable, env, EnvironmentVariableCollection, EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableMutatorType, EnvironmentVariableScope, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalExitReason, TerminalOptions, TerminalState, UIKind, Uri, window, workspace } from 'vscode'; +import { commands, ConfigurationTarget, Disposable, env, EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableMutatorType, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, GlobalEnvironmentVariableCollection, Pseudoterminal, Terminal, TerminalDimensions, TerminalExitReason, TerminalOptions, TerminalState, UIKind, Uri, window, workspace } from 'vscode'; import { assertNoRpc, poll } from '../utils'; // Disable terminal tests: @@ -913,10 +913,10 @@ import { assertNoRpc, poll } from '../utils'; test('get and forEach should work (scope)', () => { // TODO: Remove cast once `envCollectionWorkspace` API is finalized. - const collection = extensionContext.environmentVariableCollection as (EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection }); + const collection = extensionContext.environmentVariableCollection as GlobalEnvironmentVariableCollection; disposables.push({ dispose: () => collection.clear() }); const scope = { workspaceFolder: { uri: Uri.file('workspace1'), name: 'workspace1', index: 0 } }; - const scopedCollection = collection.getScopedEnvironmentVariableCollection(scope); + const scopedCollection = collection.getScoped(scope); scopedCollection.replace('A', 'scoped~a2~'); scopedCollection.append('B', 'scoped~b2~'); scopedCollection.prepend('C', 'scoped~c2~'); @@ -928,7 +928,7 @@ import { assertNoRpc, poll } from '../utils'; applyAtProcessCreation: true, applyAtShellIntegration: false }; - const expectedScopedCollection = collection.getScopedEnvironmentVariableCollection(scope); + const expectedScopedCollection = collection.getScoped(scope); deepStrictEqual(expectedScopedCollection.get('A'), { value: 'scoped~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions }); deepStrictEqual(expectedScopedCollection.get('B'), { value: 'scoped~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions }); deepStrictEqual(expectedScopedCollection.get('C'), { value: 'scoped~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions }); diff --git a/extensions/vscode-api-tests/testWorkspace/myFile.ts b/extensions/vscode-api-tests/testWorkspace/myFile.ts new file mode 100644 index 00000000000..2a2a4927869 --- /dev/null +++ b/extensions/vscode-api-tests/testWorkspace/myFile.ts @@ -0,0 +1,3 @@ +// 1 +// 2 +// 3 diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 815d839a79a..08ae5fb06c3 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -228,10 +228,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@^5.2.0-dev.20230807: - version "5.2.0-dev.20230807" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.0-dev.20230807.tgz#652fa8a3fcc46958035d9b911c815299deb9f58e" - integrity sha512-sF8sZl3r/mpAdKAxASaWaoU+mNPF3g8OrZ601HraU2l4WEwAf6nO7sq0Bzl+Y3FV7sWjy+gb6kdM1CtLjVne/g== +typescript@^5.2.1-rc: + version "5.2.1-rc" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.1-rc.tgz#9cf33ff6bc39ba9e1fa59761124f596ecf5e0c07" + integrity sha512-gsOdmedQZEWLrYhNqHuzPmcV+4wX7UujzYqszDC5mVMjcN6Nm7lN2eAtndmjWl24aGdAwJqL2ooywkxpaTx8QQ== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index 5ecb6e3e82c..584247e9fa4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.82.0", - "distro": "510a8d740286d6895197995d1edc978ffe23cadf", + "distro": "1591281180fd2cd18935e6847131d2d4213b7b69", "author": { "name": "Microsoft Corporation" }, @@ -65,8 +65,8 @@ "perf": "node scripts/code-perf.js" }, "dependencies": { - "@microsoft/1ds-core-js": "^4.0.1", - "@microsoft/1ds-post-js": "^4.0.1", + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", @@ -95,14 +95,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.3.0-beta.39", - "xterm-addon-canvas": "0.5.0-beta.9", - "xterm-addon-image": "0.6.0-beta.1", - "xterm-addon-search": "0.13.0-beta.7", - "xterm-addon-serialize": "0.11.0-beta.7", - "xterm-addon-unicode11": "0.5.0", - "xterm-addon-webgl": "0.16.0-beta.13", - "xterm-headless": "5.3.0-beta.39", + "xterm": "5.3.0-beta.61", + "xterm-addon-canvas": "0.5.0-beta.22", + "xterm-addon-image": "0.6.0-beta.14", + "xterm-addon-search": "0.13.0-beta.20", + "xterm-addon-serialize": "0.11.0-beta.20", + "xterm-addon-unicode11": "0.6.0-beta.12", + "xterm-addon-webgl": "0.16.0-beta.30", + "xterm-headless": "5.3.0-beta.61", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -150,7 +150,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "25.4.0", + "electron": "25.5.0", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^39.3.2", @@ -212,7 +212,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.2.0-dev.20230807", + "typescript": "^5.3.0-dev.20230816", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/remote/package.json b/remote/package.json index e2407e66782..8fe0e55c1de 100644 --- a/remote/package.json +++ b/remote/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "private": true, "dependencies": { - "@microsoft/1ds-core-js": "^4.0.1", - "@microsoft/1ds-post-js": "^4.0.1", + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.17.2", @@ -27,14 +27,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.3.0-beta.39", - "xterm-addon-canvas": "0.5.0-beta.9", - "xterm-addon-image": "0.6.0-beta.1", - "xterm-addon-search": "0.13.0-beta.7", - "xterm-addon-serialize": "0.11.0-beta.7", - "xterm-addon-unicode11": "0.5.0", - "xterm-addon-webgl": "0.16.0-beta.13", - "xterm-headless": "5.3.0-beta.39", + "xterm": "5.3.0-beta.61", + "xterm-addon-canvas": "0.5.0-beta.22", + "xterm-addon-image": "0.6.0-beta.14", + "xterm-addon-search": "0.13.0-beta.20", + "xterm-addon-serialize": "0.11.0-beta.20", + "xterm-addon-unicode11": "0.6.0-beta.12", + "xterm-addon-webgl": "0.16.0-beta.30", + "xterm-headless": "5.3.0-beta.61", "yauzl": "^2.9.2", "yazl": "^2.4.3" } diff --git a/remote/web/package.json b/remote/web/package.json index a4bd45b2be5..6075a563e34 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -3,19 +3,19 @@ "version": "0.0.0", "private": true, "dependencies": { - "@microsoft/1ds-core-js": "^4.0.1", - "@microsoft/1ds-post-js": "^4.0.1", + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", "jschardet": "3.0.0", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", "vscode-textmate": "9.0.0", - "xterm": "5.3.0-beta.39", - "xterm-addon-canvas": "0.5.0-beta.9", - "xterm-addon-image": "0.6.0-beta.1", - "xterm-addon-search": "0.13.0-beta.7", - "xterm-addon-unicode11": "0.5.0", - "xterm-addon-webgl": "0.16.0-beta.13" + "xterm": "5.3.0-beta.61", + "xterm-addon-canvas": "0.5.0-beta.22", + "xterm-addon-image": "0.6.0-beta.14", + "xterm-addon-search": "0.13.0-beta.20", + "xterm-addon-unicode11": "0.6.0-beta.12", + "xterm-addon-webgl": "0.16.0-beta.30" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 9fe8c502181..b56fac51a45 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -2,63 +2,41 @@ # yarn lockfile v1 -"@microsoft/1ds-core-js@4.0.1", "@microsoft/1ds-core-js@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-4.0.1.tgz#6fcf129276c933934d618f7fc3de49613259e3ec" - integrity sha512-BcTKhcRixCqLYEo1xVlQofvvZpgey9qmkzxQd1j9bBZS4iQl9IOmGBzlf/vGSITC7VXtEk2NZOlHQN0llAIfSw== +"@microsoft/1ds-core-js@3.2.13", "@microsoft/1ds-core-js@^3.2.13": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz#0c105ed75091bae3f1555c0334704fa9911c58fb" + integrity sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg== dependencies: - "@microsoft/applicationinsights-core-js" "3.0.2" - "@microsoft/applicationinsights-shims" "^3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.6 < 2.x" + "@microsoft/applicationinsights-core-js" "2.8.15" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/1ds-post-js@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-4.0.1.tgz#8630927d10518067686104baec98557b74317e4e" - integrity sha512-uyYkG9mxzzj/4HgxVJsgcz9abfakW+Rjrsnor/hXS5I5ezE3YUX+54DLvAh/c6mEgoDI7u8wGkGl3HgCtAYbDQ== +"@microsoft/1ds-post-js@^3.2.13": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz#560aacac8a92fdbb79e8c2ebcb293d56e19f51aa" + integrity sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA== dependencies: - "@microsoft/1ds-core-js" "4.0.1" - "@microsoft/applicationinsights-shims" "^3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.6 < 2.x" + "@microsoft/1ds-core-js" "3.2.13" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/applicationinsights-core-js@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz#108e20df8c162bec92b1f66f9de2530a25d9f51a" - integrity sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ== +"@microsoft/applicationinsights-core-js@2.8.15": + version "2.8.15" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz#8fa466474260e01967fe649f14dd9e5ff91dcdc8" + integrity sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ== dependencies: - "@microsoft/applicationinsights-shims" "3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.5 < 2.x" + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.9" -"@microsoft/applicationinsights-shims@3.0.1", "@microsoft/applicationinsights-shims@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz#3865b73ace8405b9c4618cc5c571f2fe3876f06f" - integrity sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg== - dependencies: - "@nevware21/ts-utils" ">= 0.9.4 < 2.x" - -"@microsoft/dynamicproto-js@^2.0.2": +"@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2": version "2.0.2" - resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz#e57fbec2e7067d48b7e8e1e1c1d354028ef718a6" - integrity sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg== - dependencies: - "@nevware21/ts-utils" ">= 0.9.4 < 2.x" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" + integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== -"@nevware21/ts-async@>= 0.2.4 < 2.x": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@nevware21/ts-async/-/ts-async-0.2.6.tgz#9fad1c4a56b01430b407d454817f68bb6ace92b0" - integrity sha512-NCUqEZSbsy7LVtKlUScd/eTst6djkWauLlzoIPVKCOxalEBdO8lrgNRIm4Xy68JNudNN5faqa2WA12X8m0BVhA== - dependencies: - "@nevware21/ts-utils" ">= 0.9.7 < 2.x" - -"@nevware21/ts-utils@>= 0.9.4 < 2.x", "@nevware21/ts-utils@>= 0.9.5 < 2.x", "@nevware21/ts-utils@>= 0.9.6 < 2.x", "@nevware21/ts-utils@>= 0.9.7 < 2.x": - version "0.9.8" - resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.9.8.tgz#6db23211a5c7539d3b4362e273087effcfc7d2f8" - integrity sha512-kZ8s8hcn9jPVX/M7kSsBYrOGlHjqLahmxrG7QeKTk5paeVwfgKdvVCjj5Acb4UGb/ukU1G34U1Z3eb7bbVanyA== +"@microsoft/dynamicproto-js@^1.1.7", "@microsoft/dynamicproto-js@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6" + integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ== "@vscode/iconv-lite-umd@0.7.0": version "0.7.0" @@ -90,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.5.0-beta.9: - version "0.5.0-beta.9" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.9.tgz#d4a9536cf586f78a54527751e03abf6445613886" - integrity sha512-AMyFctHbIWx0aLcACINuZqFFNoz4ndGtIAb4pQguIl9t8r+2otiV11MY4FJmPM0kUzEzoLWzVFJaGxOzykCWuw== +xterm-addon-canvas@0.5.0-beta.22: + version "0.5.0-beta.22" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.22.tgz#513f0c2b7cf96073f47627b27e8965c1b1a22431" + integrity sha512-9F6ZI0DMRgffVfHkLkDwl5n8VscvCaV10tWI3skXOX7Y7Aws6OEeglkOPoU3IllofCU792kHKM4pPoToUxTltg== -xterm-addon-image@0.6.0-beta.1: - version "0.6.0-beta.1" - resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.1.tgz#058c171a4866f1cde7608e3b00050476b55ece4c" - integrity sha512-vLxUEo/KAsEaRhMshJ4uxnfc/Si8YfT3kilaK5WuiJflVODzQjytzZrl2iLeZaWKiqQp0NR6AEhuXlYASK675A== +xterm-addon-image@0.6.0-beta.14: + version "0.6.0-beta.14" + resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.14.tgz#75fc3f824123183a4bbb5306e22f8b2c6966b0a6" + integrity sha512-D5Gh5JTKhHaPt1KwQNf6diF37KA4eToJw3XId1wy62tWmSqfq+QflhOGTfd+SnSQYCktU05ETzM+0tncIU62pQ== -xterm-addon-search@0.13.0-beta.7: - version "0.13.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.7.tgz#2f9bf9d8380c4c40f021c47dc583aa32bea8796e" - integrity sha512-Dnuws0ThaqzxRDo8TjkSb5WlMYycONVDSr/irx2pk2I4vC5RuSpOifmD46ALlryNSDkMasjfCLjHPILHsFDCfw== +xterm-addon-search@0.13.0-beta.20: + version "0.13.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.20.tgz#8ddd0513e2a70fcefa325722100d2e1bfaf3b9cb" + integrity sha512-wrx6187cJ1UenGL6ZeYv3jFvRPhhENTfbC+Hv1Fnww8LmsKhcj+0+Pm6yInNjX/9hNVsNzdqKyqNeEMoykyoyA== -xterm-addon-unicode11@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0.tgz#41c0d96acc1e3bb6c6596eee64e163b6bca74be7" - integrity sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg== +xterm-addon-unicode11@0.6.0-beta.12: + version "0.6.0-beta.12" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.6.0-beta.12.tgz#ac6df9d635325dc692e4c602e74a2fc27a09405c" + integrity sha512-9wWWf/5nFafYgq0pn9EgAWnXaXGleVxfjNOqavpLRYFv0nw42QbaYyGvnGcxyYHM5Aqx/8rYE/DDVWZBqQZdYA== -xterm-addon-webgl@0.16.0-beta.13: - version "0.16.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.13.tgz#ffacd22b8d86ca7f9ba903a64bc85654523955fb" - integrity sha512-1avJBoAKl+0dOjRK0pvu2im6gaXyP/6oJcEBQXCTz3V8YlgcG/jom8A6FEQSEDGZ/4dnyDvnLbsg3+aLZIr6gw== +xterm-addon-webgl@0.16.0-beta.30: + version "0.16.0-beta.30" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.30.tgz#820d5c65f868b14ec4177bfb8a294931a53616bf" + integrity sha512-39qPHPFmNENxcHf8/CzGHS6wzKMMegoRkHB1+scqtBhSxFaD8tX5Ye33HZIEdQ9nXe9xtr4FWVp77T+n9hdrew== -xterm@5.3.0-beta.39: - version "5.3.0-beta.39" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.39.tgz#b64da3fd1b374fd535db61294a49c16453dbb77e" - integrity sha512-Z1r0ijTgad5MjFoz8qZ+2Qc3bgjG6kYY/kZI3DTA1pibArHeOMDYk9+IeCb9adF74zvZurjytRfXKJgX6M9hwg== +xterm@5.3.0-beta.61: + version "5.3.0-beta.61" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.61.tgz#a6c27d90a5314da51d80deeb32f3bd77f1e1c8f6" + integrity sha512-rJHpCc48GSpHnu0SSERynQ80D5ikvFVsqhv6JdmeONTrnAFRr134OglJRIpbi2YK8UPbV6F6Dfqm/AQh+9GZzA== diff --git a/remote/yarn.lock b/remote/yarn.lock index 8ad9cc2907f..4716df355d5 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -2,63 +2,41 @@ # yarn lockfile v1 -"@microsoft/1ds-core-js@4.0.1", "@microsoft/1ds-core-js@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-4.0.1.tgz#6fcf129276c933934d618f7fc3de49613259e3ec" - integrity sha512-BcTKhcRixCqLYEo1xVlQofvvZpgey9qmkzxQd1j9bBZS4iQl9IOmGBzlf/vGSITC7VXtEk2NZOlHQN0llAIfSw== +"@microsoft/1ds-core-js@3.2.13", "@microsoft/1ds-core-js@^3.2.13": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz#0c105ed75091bae3f1555c0334704fa9911c58fb" + integrity sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg== dependencies: - "@microsoft/applicationinsights-core-js" "3.0.2" - "@microsoft/applicationinsights-shims" "^3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.6 < 2.x" + "@microsoft/applicationinsights-core-js" "2.8.15" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/1ds-post-js@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-4.0.1.tgz#8630927d10518067686104baec98557b74317e4e" - integrity sha512-uyYkG9mxzzj/4HgxVJsgcz9abfakW+Rjrsnor/hXS5I5ezE3YUX+54DLvAh/c6mEgoDI7u8wGkGl3HgCtAYbDQ== +"@microsoft/1ds-post-js@^3.2.13": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz#560aacac8a92fdbb79e8c2ebcb293d56e19f51aa" + integrity sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA== dependencies: - "@microsoft/1ds-core-js" "4.0.1" - "@microsoft/applicationinsights-shims" "^3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.6 < 2.x" + "@microsoft/1ds-core-js" "3.2.13" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/applicationinsights-core-js@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz#108e20df8c162bec92b1f66f9de2530a25d9f51a" - integrity sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ== +"@microsoft/applicationinsights-core-js@2.8.15": + version "2.8.15" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz#8fa466474260e01967fe649f14dd9e5ff91dcdc8" + integrity sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ== dependencies: - "@microsoft/applicationinsights-shims" "3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.5 < 2.x" + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.9" -"@microsoft/applicationinsights-shims@3.0.1", "@microsoft/applicationinsights-shims@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz#3865b73ace8405b9c4618cc5c571f2fe3876f06f" - integrity sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg== - dependencies: - "@nevware21/ts-utils" ">= 0.9.4 < 2.x" - -"@microsoft/dynamicproto-js@^2.0.2": +"@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2": version "2.0.2" - resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz#e57fbec2e7067d48b7e8e1e1c1d354028ef718a6" - integrity sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg== - dependencies: - "@nevware21/ts-utils" ">= 0.9.4 < 2.x" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" + integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== -"@nevware21/ts-async@>= 0.2.4 < 2.x": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@nevware21/ts-async/-/ts-async-0.2.6.tgz#9fad1c4a56b01430b407d454817f68bb6ace92b0" - integrity sha512-NCUqEZSbsy7LVtKlUScd/eTst6djkWauLlzoIPVKCOxalEBdO8lrgNRIm4Xy68JNudNN5faqa2WA12X8m0BVhA== - dependencies: - "@nevware21/ts-utils" ">= 0.9.7 < 2.x" - -"@nevware21/ts-utils@>= 0.9.4 < 2.x", "@nevware21/ts-utils@>= 0.9.5 < 2.x", "@nevware21/ts-utils@>= 0.9.6 < 2.x", "@nevware21/ts-utils@>= 0.9.7 < 2.x": - version "0.9.8" - resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.9.8.tgz#6db23211a5c7539d3b4362e273087effcfc7d2f8" - integrity sha512-kZ8s8hcn9jPVX/M7kSsBYrOGlHjqLahmxrG7QeKTk5paeVwfgKdvVCjj5Acb4UGb/ukU1G34U1Z3eb7bbVanyA== +"@microsoft/dynamicproto-js@^1.1.7", "@microsoft/dynamicproto-js@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6" + integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ== "@parcel/watcher@2.1.0": version "2.1.0" @@ -899,45 +877,45 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-canvas@0.5.0-beta.9: - version "0.5.0-beta.9" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.9.tgz#d4a9536cf586f78a54527751e03abf6445613886" - integrity sha512-AMyFctHbIWx0aLcACINuZqFFNoz4ndGtIAb4pQguIl9t8r+2otiV11MY4FJmPM0kUzEzoLWzVFJaGxOzykCWuw== +xterm-addon-canvas@0.5.0-beta.22: + version "0.5.0-beta.22" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.22.tgz#513f0c2b7cf96073f47627b27e8965c1b1a22431" + integrity sha512-9F6ZI0DMRgffVfHkLkDwl5n8VscvCaV10tWI3skXOX7Y7Aws6OEeglkOPoU3IllofCU792kHKM4pPoToUxTltg== -xterm-addon-image@0.6.0-beta.1: - version "0.6.0-beta.1" - resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.1.tgz#058c171a4866f1cde7608e3b00050476b55ece4c" - integrity sha512-vLxUEo/KAsEaRhMshJ4uxnfc/Si8YfT3kilaK5WuiJflVODzQjytzZrl2iLeZaWKiqQp0NR6AEhuXlYASK675A== +xterm-addon-image@0.6.0-beta.14: + version "0.6.0-beta.14" + resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.14.tgz#75fc3f824123183a4bbb5306e22f8b2c6966b0a6" + integrity sha512-D5Gh5JTKhHaPt1KwQNf6diF37KA4eToJw3XId1wy62tWmSqfq+QflhOGTfd+SnSQYCktU05ETzM+0tncIU62pQ== -xterm-addon-search@0.13.0-beta.7: - version "0.13.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.7.tgz#2f9bf9d8380c4c40f021c47dc583aa32bea8796e" - integrity sha512-Dnuws0ThaqzxRDo8TjkSb5WlMYycONVDSr/irx2pk2I4vC5RuSpOifmD46ALlryNSDkMasjfCLjHPILHsFDCfw== +xterm-addon-search@0.13.0-beta.20: + version "0.13.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.20.tgz#8ddd0513e2a70fcefa325722100d2e1bfaf3b9cb" + integrity sha512-wrx6187cJ1UenGL6ZeYv3jFvRPhhENTfbC+Hv1Fnww8LmsKhcj+0+Pm6yInNjX/9hNVsNzdqKyqNeEMoykyoyA== -xterm-addon-serialize@0.11.0-beta.7: - version "0.11.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.11.0-beta.7.tgz#934a48a4490c2ca94a5cfcdd646fb0813b4e56d0" - integrity sha512-TwYsEHq6TflY0vmq3yAI2VKNqzEAZ0gCm3TThDMPrPQiv4LcewQSeGu9RujEqF9RAhujhceaM3GajBxySAWyqQ== +xterm-addon-serialize@0.11.0-beta.20: + version "0.11.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.11.0-beta.20.tgz#e879b34d214761403f1081833f9221c6903bf0c3" + integrity sha512-OXnC1SATaz7kEFjFWhyv9MJaXi8yHdPjazpGLNi11h33CRTKtCQiqqPBHU87dztnXmpEX6Jw0/jr3zlyXuAmnw== -xterm-addon-unicode11@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0.tgz#41c0d96acc1e3bb6c6596eee64e163b6bca74be7" - integrity sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg== +xterm-addon-unicode11@0.6.0-beta.12: + version "0.6.0-beta.12" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.6.0-beta.12.tgz#ac6df9d635325dc692e4c602e74a2fc27a09405c" + integrity sha512-9wWWf/5nFafYgq0pn9EgAWnXaXGleVxfjNOqavpLRYFv0nw42QbaYyGvnGcxyYHM5Aqx/8rYE/DDVWZBqQZdYA== -xterm-addon-webgl@0.16.0-beta.13: - version "0.16.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.13.tgz#ffacd22b8d86ca7f9ba903a64bc85654523955fb" - integrity sha512-1avJBoAKl+0dOjRK0pvu2im6gaXyP/6oJcEBQXCTz3V8YlgcG/jom8A6FEQSEDGZ/4dnyDvnLbsg3+aLZIr6gw== +xterm-addon-webgl@0.16.0-beta.30: + version "0.16.0-beta.30" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.30.tgz#820d5c65f868b14ec4177bfb8a294931a53616bf" + integrity sha512-39qPHPFmNENxcHf8/CzGHS6wzKMMegoRkHB1+scqtBhSxFaD8tX5Ye33HZIEdQ9nXe9xtr4FWVp77T+n9hdrew== -xterm-headless@5.3.0-beta.39: - version "5.3.0-beta.39" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.39.tgz#84a509dbe1dc9b2229b5274ae3430ce968986346" - integrity sha512-Q4vfqAaYoWh7/T4HearlLlKkn9vjN5EugsNyuPd6hq+Ug1FwJIJYj+I9vY67+Rqxr24XGnrQdXfvvEas29KXsw== +xterm-headless@5.3.0-beta.61: + version "5.3.0-beta.61" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.61.tgz#28654550cb572709b99ea3eb8672d4568ae141c9" + integrity sha512-yfkbPLUtKjE4K7DsZ204A1BuOKpu6Usqi6rIYWT4XRMi+LjnkTbBjGr2BSjyJ3Gmtm+cSgBD0SvRN+V3xNxbxA== -xterm@5.3.0-beta.39: - version "5.3.0-beta.39" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.39.tgz#b64da3fd1b374fd535db61294a49c16453dbb77e" - integrity sha512-Z1r0ijTgad5MjFoz8qZ+2Qc3bgjG6kYY/kZI3DTA1pibArHeOMDYk9+IeCb9adF74zvZurjytRfXKJgX6M9hwg== +xterm@5.3.0-beta.61: + version "5.3.0-beta.61" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.61.tgz#a6c27d90a5314da51d80deeb32f3bd77f1e1c8f6" + integrity sha512-rJHpCc48GSpHnu0SSERynQ80D5ikvFVsqhv6JdmeONTrnAFRr134OglJRIpbi2YK8UPbV6F6Dfqm/AQh+9GZzA== yallist@^4.0.0: version "4.0.0" diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 256f406fb6c..b7c5d7468b5 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -32,6 +32,8 @@ "vs/platform/files/browser/htmlFileSystemProvider.ts", "vs/platform/files/browser/webFileSystemAccess.ts", "vs/platform/telemetry/*", - "vs/platform/assignment/*" + "vs/platform/assignment/*", + "vs/platform/terminal/*", + "vs/platform/externalTerminal/*" ] } diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index c89c6a7a063..9445c64286a 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -705,6 +705,7 @@ class BranchNode implements ISplitView, IDisposable { this.splitviewSashResetDisposable.dispose(); this.childrenSashResetDisposable.dispose(); this.childrenChangeDisposable.dispose(); + this.onDidScrollDisposable.dispose(); this.splitview.dispose(); } } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 36c5a74a948..ee900068b4b 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -16,7 +16,7 @@ import { timeout } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event, EventBufferer } from 'vs/base/common/event'; -import { matchesPrefix } from 'vs/base/common/filters'; +import { matchesFuzzy2, matchesPrefix } from 'vs/base/common/filters'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; @@ -527,11 +527,27 @@ class TypeNavigationController implements IDisposable { const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index)); const labelStr = label && label.toString(); - if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) { - this.previouslyFocused = start; - this.list.setFocus([index]); - this.list.reveal(index); - return; + if (this.list.options.typeNavigationEnabled) { + if (typeof labelStr !== 'undefined') { + const prefix = matchesPrefix(word, labelStr); + const fuzzy = matchesFuzzy2(word, labelStr); + + // ensures that when fuzzy matching, it doesn't clash with prefix matching (1 input vs 1+ should be prefix and fuzzy respecitvely) + const fuzzyScore = fuzzy ? fuzzy[0].end - fuzzy[0].start : 0; + if (prefix || fuzzyScore > 1) { + this.previouslyFocused = start; + this.list.setFocus([index]); + this.list.reveal(index); + return; + } + } + } else { + if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) { + this.previouslyFocused = start; + this.list.setFocus([index]); + this.list.reveal(index); + return; + } } } } @@ -978,6 +994,7 @@ export interface IListOptions extends IListOptionsUpdate { readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider; readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate; readonly keyboardSupport?: boolean; + readonly keyboardNavigationEnabled?: boolean; readonly multipleSelectionController?: IMultipleSelectionController; readonly styleController?: (suffix: string) => IStyleController; readonly accessibilityProvider?: IListAccessibilityProvider; diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index b822db43751..28f18d42537 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -565,11 +565,11 @@ export class SplitView extends Disposable { this.sashContainer = append(this.el, $('.sash-container')); this.viewContainer = $('.split-view-container'); - this.scrollable = new Scrollable({ + this.scrollable = this._register(new Scrollable({ forceIntegerValues: true, smoothScrollDuration: 125, scheduleAtNextAnimationFrame - }); + })); this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, { vertical: this.orientation === Orientation.VERTICAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden, horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 1a6089b5958..bcb90361b41 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -52,7 +52,7 @@ export class ToolBar extends Disposable { private _onDidChangeDropdownVisibility = this._register(new EventMultiplexer()); readonly onDidChangeDropdownVisibility = this._onDidChangeDropdownVisibility.event; - private disposables = new DisposableStore(); + private disposables = this._register(new DisposableStore()); constructor(container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) { super(); diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index a54c2c59f9c..22c2859f5e9 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -707,6 +707,8 @@ export function tieBreakComparators(...comparators: Comparator[]): */ export const numberComparator: Comparator = (a, b) => a - b; +export const booleanComparator: Comparator = (a, b) => numberComparator(a ? 1 : 0, b ? 1 : 0); + export function reverseOrder(comparator: Comparator): Comparator { return (a, b) => -comparator(a, b); } @@ -754,6 +756,21 @@ export function findMinBy(items: readonly T[], comparator: Comparator): T return findMaxBy(items, (a, b) => -comparator(a, b)); } +export function findMaxIdxBy(items: readonly T[], comparator: Comparator): number { + if (items.length === 0) { + return -1; + } + + let maxIdx = 0; + for (let i = 1; i < items.length; i++) { + const item = items[i]; + if (comparator(item, items[maxIdx]) > 0) { + maxIdx = i; + } + } + return maxIdx; +} + export class ArrayQueue { private firstIdx = 0; private lastIdx = this.items.length - 1; diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 308e03bfcd7..f06dcf7acbc 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -47,7 +47,7 @@ export namespace Iterable { return false; } - export function find(iterable: Iterable, predicate: (t: T) => t is R): T | undefined; + export function find(iterable: Iterable, predicate: (t: T) => t is R): R | undefined; export function find(iterable: Iterable, predicate: (t: T) => boolean): T | undefined; export function find(iterable: Iterable, predicate: (t: T) => boolean): T | undefined { for (const element of iterable) { diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index 67a29703cdc..a51f0094591 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; -import { regExpFlags } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import { MarshalledId } from './marshallingIds'; @@ -28,7 +27,7 @@ function replacer(key: string, value: any): any { return { $mid: MarshalledId.Regexp, source: value.source, - flags: regExpFlags(value), + flags: value.flags, }; } return value; diff --git a/src/vs/base/common/stream.ts b/src/vs/base/common/stream.ts index 9f1039c0e52..055558fc748 100644 --- a/src/vs/base/common/stream.ts +++ b/src/vs/base/common/stream.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; /** * The payload that flows in readable stream events. @@ -567,17 +568,16 @@ export interface IStreamListener { /** * Helper to listen to all events of a T stream in proper order. */ -export function listenStream(stream: ReadableStreamEvents, listener: IStreamListener): IDisposable { - let destroyed = false; +export function listenStream(stream: ReadableStreamEvents, listener: IStreamListener, token?: CancellationToken): void { stream.on('error', error => { - if (!destroyed) { + if (!token?.isCancellationRequested) { listener.onError(error); } }); stream.on('end', () => { - if (!destroyed) { + if (!token?.isCancellationRequested) { listener.onEnd(); } }); @@ -586,12 +586,10 @@ export function listenStream(stream: ReadableStreamEvents, listener: IStre // into flowing mode. As such it is important to // add this listener last (DO NOT CHANGE!) stream.on('data', data => { - if (!destroyed) { + if (!token?.isCancellationRequested) { listener.onData(data); } }); - - return toDisposable(() => destroyed = true); } /** diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 5837c2f4c49..02d9179c792 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -217,17 +217,6 @@ export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean { return !!(match && regexp.lastIndex === 0); } -export function regExpContainsBackreference(regexpValue: string): boolean { - return !!regexpValue.match(/([^\\]|^)(\\\\)*\\\d+/); -} - -export function regExpFlags(regexp: RegExp): string { - return (regexp.global ? 'g' : '') - + (regexp.ignoreCase ? 'i' : '') - + (regexp.multiline ? 'm' : '') - + ((regexp as any /* standalone editor compilation */).unicode ? 'u' : ''); -} - export function splitLines(str: string): string[] { return str.split(/\r\n|\r|\n/); } diff --git a/src/vs/base/test/common/snapshot.ts b/src/vs/base/test/common/snapshot.ts new file mode 100644 index 00000000000..72bbfb8b593 --- /dev/null +++ b/src/vs/base/test/common/snapshot.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Lazy } from 'vs/base/common/lazy'; +import { FileAccess } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; + +declare const __readFileInTests: (path: string) => Promise; +declare const __writeFileInTests: (path: string, contents: string) => Promise; +declare const __readDirInTests: (path: string) => Promise; +declare const __unlinkInTests: (path: string) => Promise; +declare const __mkdirPInTests: (path: string) => Promise; + +// setup on import so assertSnapshot has the current context without explicit passing +let context: Lazy | undefined; +const sanitizeName = (name: string) => name.replace(/[^a-z0-9_-]/gi, '_'); +const normalizeCrlf = (str: string) => str.replace(/\r\n/g, '\n'); + +export interface ISnapshotOptions { + /** Name for snapshot file, rather than an incremented number */ + name?: string; + /** Extension name of the snapshot file, defaults to `.snap` */ + extension?: string; +} + +/** + * This is exported only for tests against the snapshotting itself! Use + * {@link assertSnapshot} as a consumer! + */ +export class SnapshotContext { + private nextIndex = 0; + protected snapshotsDir: URI; + private readonly namePrefix: string; + private readonly usedNames = new Set(); + + constructor(private readonly test: Mocha.Test | undefined) { + if (!test) { + throw new Error('assertSnapshot can only be used in a test'); + } + + if (!test.file) { + throw new Error('currentTest.file is not set, please open an issue with the test you\'re trying to run'); + } + + const src = FileAccess.asFileUri(''); + const parts = test.file.split(/[/\\]/g); + + this.namePrefix = sanitizeName(test.fullTitle()) + '_'; + this.snapshotsDir = URI.joinPath(src, ...[...parts.slice(0, -1), '__snapshots__']); + } + + public async assert(value: any, options?: ISnapshotOptions) { + const originalStack = new Error().stack!; // save to make the stack nicer on failure + const nameOrIndex = (options?.name ? sanitizeName(options.name) : this.nextIndex++); + const fileName = this.namePrefix + nameOrIndex + '.' + (options?.extension || 'snap'); + this.usedNames.add(fileName); + + const fpath = URI.joinPath(this.snapshotsDir, fileName).fsPath; + const actual = formatValue(value); + let expected: string; + try { + expected = await __readFileInTests(fpath); + } catch { + console.info(`Creating new snapshot in: ${fpath}`); + await __mkdirPInTests(this.snapshotsDir.fsPath); + await __writeFileInTests(fpath, actual); + return; + } + + if (normalizeCrlf(expected) !== normalizeCrlf(actual)) { + await __writeFileInTests(fpath + '.actual', actual); + const err: any = new Error(`Snapshot #${nameOrIndex} does not match expected output`); + err.expected = expected; + err.actual = actual; + err.snapshotPath = fpath; + err.stack = (err.stack as string) + .split('\n') + // remove all frames from the async stack and keep the original caller's frame + .slice(0, 1) + .concat(originalStack.split('\n').slice(3)) + .join('\n'); + throw err; + } + } + + public async removeOldSnapshots() { + const contents = await __readDirInTests(this.snapshotsDir.fsPath); + const toDelete = contents.filter(f => f.startsWith(this.namePrefix) && !this.usedNames.has(f)); + if (toDelete.length) { + console.info(`Deleting ${toDelete.length} old snapshots for ${this.test?.fullTitle()}`); + } + + await Promise.all(toDelete.map(f => __unlinkInTests(URI.joinPath(this.snapshotsDir, f).fsPath))); + } +} + +const debugDescriptionSymbol = Symbol.for('debug.description'); + +function formatValue(value: unknown, level = 0, seen: unknown[] = []): string { + switch (typeof value) { + case 'bigint': + case 'boolean': + case 'number': + case 'symbol': + case 'undefined': + return String(value); + case 'string': + return level === 0 ? value : JSON.stringify(value); + case 'function': + return `[Function ${value.name}]`; + case 'object': { + if (value === null) { + return 'null'; + } + if (value instanceof RegExp) { + return String(value); + } + if (seen.includes(value)) { + return '[Circular]'; + } + if (debugDescriptionSymbol in value && typeof (value as any)[debugDescriptionSymbol] === 'function') { + return (value as any)[debugDescriptionSymbol](); + } + const oi = ' '.repeat(level); + const ci = ' '.repeat(level + 1); + if (Array.isArray(value)) { + const children = value.map(v => formatValue(v, level + 1, [...seen, value])); + const multiline = children.some(c => c.includes('\n')) || children.join(', ').length > 80; + return multiline ? `[\n${ci}${children.join(`,\n${ci}`)}\n${oi}]` : `[ ${children.join(', ')} ]`; + } + + let entries; + let prefix = ''; + if (value instanceof Map) { + prefix = 'Map '; + entries = [...value.entries()]; + } else if (value instanceof Set) { + prefix = 'Set '; + entries = [...value.entries()]; + } else { + entries = Object.entries(value); + } + + const lines = entries.map(([k, v]) => `${k}: ${formatValue(v, level + 1, [...seen, value])}`); + return prefix + (lines.length > 1 + ? `{\n${ci}${lines.join(`,\n${ci}`)}\n${oi}}` + : `{ ${lines.join(',\n')} }`); + } + default: + throw new Error(`Unknown type ${value}`); + } +} + +setup(function () { + const currentTest = this.currentTest; + context = new Lazy(() => new SnapshotContext(currentTest)); +}); +teardown(async function () { + if (this.currentTest?.state === 'passed') { + await context?.rawValue?.removeOldSnapshots(); + } + context = undefined; +}); + +/** + * Implements a snapshot testing utility. āš ļø This is async! āš ļø + * + * The first time a snapshot test is run, it'll record the value it's called + * with as the expected value. Subsequent runs will fail if the value differs, + * but the snapshot can be regenerated by hand or using the Selfhost Test + * Provider Extension which'll offer to update it. + * + * The snapshot will be associated with the currently running test and stored + * in a `__snapshots__` directory next to the test file, which is expected to + * be the first `.test.js` file in the callstack. + */ +export function assertSnapshot(value: any, options?: ISnapshotOptions): Promise { + if (!context) { + throw new Error('assertSnapshot can only be used in a test'); + } + + return context.value.assert(value, options); +} diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 78c38d691a4..63d30530a95 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { consumeReadable, consumeStream, isReadable, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, prefixedReadable, prefixedStream, Readable, ReadableStream, toReadable, toStream, transform } from 'vs/base/common/stream'; suite('Stream', () => { @@ -351,14 +352,16 @@ suite('Stream', () => { assert.strictEqual(end, true); }); - test('listenStream - dispose', () => { + test('listenStream - cancellation', () => { const stream = newWriteableStream(strings => strings.join()); let error = false; let end = false; let data = ''; - const disposable = listenStream(stream, { + const cts = new CancellationTokenSource(); + + listenStream(stream, { onData: d => { data = d; }, @@ -368,9 +371,9 @@ suite('Stream', () => { onEnd: () => { end = true; } - }); + }, cts.token); - disposable.dispose(); + cts.cancel(); stream.write('Hello'); assert.strictEqual(data, ''); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index f4bea3785b8..18e53a2e5d0 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -265,22 +265,6 @@ suite('Strings', () => { assert(regExpWithFlags.multiline); }); - test('regExpContainsBackreference', () => { - assert(strings.regExpContainsBackreference('foo \\5 bar')); - assert(strings.regExpContainsBackreference('\\2')); - assert(strings.regExpContainsBackreference('(\\d)(\\n)(\\1)')); - assert(strings.regExpContainsBackreference('(A).*?\\1')); - assert(strings.regExpContainsBackreference('\\\\\\1')); - assert(strings.regExpContainsBackreference('foo \\\\\\1')); - - assert(!strings.regExpContainsBackreference('')); - assert(!strings.regExpContainsBackreference('\\\\1')); - assert(!strings.regExpContainsBackreference('foo \\\\1')); - assert(!strings.regExpContainsBackreference('(A).*?\\\\1')); - assert(!strings.regExpContainsBackreference('foo \\d1 bar')); - assert(!strings.regExpContainsBackreference('123')); - }); - test('getLeadingWhitespace', () => { assert.strictEqual(strings.getLeadingWhitespace(' foo'), ' '); assert.strictEqual(strings.getLeadingWhitespace(' foo', 2), ''); diff --git a/src/vs/base/test/node/__snapshots__/snapshot_cleans_up_old_snapshots_0.snap b/src/vs/base/test/node/__snapshots__/snapshot_cleans_up_old_snapshots_0.snap new file mode 100644 index 00000000000..53d99c7989b --- /dev/null +++ b/src/vs/base/test/node/__snapshots__/snapshot_cleans_up_old_snapshots_0.snap @@ -0,0 +1,8 @@ +hello_world__0.snap: + { cool: true } +hello_world__1.snap: + { nifty: true } +hello_world__fourthTest.snap: + { customName: 2 } +hello_world__thirdTest.txt: + { customName: 1 } diff --git a/src/vs/base/test/node/__snapshots__/snapshot_cleans_up_old_snapshots_1.snap b/src/vs/base/test/node/__snapshots__/snapshot_cleans_up_old_snapshots_1.snap new file mode 100644 index 00000000000..b7106c217c2 --- /dev/null +++ b/src/vs/base/test/node/__snapshots__/snapshot_cleans_up_old_snapshots_1.snap @@ -0,0 +1,4 @@ +hello_world__0.snap: + { cool: true } +hello_world__thirdTest.snap: + { customName: 1 } diff --git a/src/vs/base/test/node/__snapshots__/snapshot_creates_a_snapshot_0.snap b/src/vs/base/test/node/__snapshots__/snapshot_creates_a_snapshot_0.snap new file mode 100644 index 00000000000..4c53158fc13 --- /dev/null +++ b/src/vs/base/test/node/__snapshots__/snapshot_creates_a_snapshot_0.snap @@ -0,0 +1,2 @@ +hello_world__0.snap: + { cool: true } diff --git a/src/vs/base/test/node/__snapshots__/snapshot_formats_object_nicely_0.snap b/src/vs/base/test/node/__snapshots__/snapshot_formats_object_nicely_0.snap new file mode 100644 index 00000000000..d5e7745d232 --- /dev/null +++ b/src/vs/base/test/node/__snapshots__/snapshot_formats_object_nicely_0.snap @@ -0,0 +1,35 @@ +[ + 1, + true, + undefined, + null, + 123, + Symbol(heyo), + "hello", + { hello: "world" }, + { a: [Circular] }, + Map { + hello: 1, + goodbye: 2 + }, + Set { + 1: 1, + 2: 2, + 3: 3 + }, + [Function helloWorld], + /hello/g, + [ + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string", + "long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string" + ], + Range [1 -> 5] +] \ No newline at end of file diff --git a/src/vs/base/test/node/snapshot.test.ts b/src/vs/base/test/node/snapshot.test.ts new file mode 100644 index 00000000000..6c12ae70dd3 --- /dev/null +++ b/src/vs/base/test/node/snapshot.test.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { tmpdir } from 'os'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { Promises } from 'vs/base/node/pfs'; +import { SnapshotContext, assertSnapshot } from 'vs/base/test/common/snapshot'; +import { URI } from 'vs/base/common/uri'; +import path = require('path'); +import { assertThrowsAsync } from 'vs/base/test/common/utils'; + +// tests for snapshot are in Node so that we can use native FS operations to +// set up and validate things. +// +// Uses snapshots for testing snapshots. It's snapception! + +suite('snapshot', () => { + let testDir: string; + + setup(function () { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'snapshot'); + return Promises.mkdir(testDir, { recursive: true }); + }); + + teardown(function () { + return Promises.rm(testDir); + }); + + const makeContext = (test: Partial | undefined) => { + return new class extends SnapshotContext { + constructor() { + super(test as Mocha.Test); + this.snapshotsDir = URI.file(testDir); + } + }; + }; + + const snapshotFileTree = async () => { + let str = ''; + + const printDir = async (dir: string, indent: number) => { + const children = await Promises.readdir(dir); + for (const child of children) { + const p = path.join(dir, child); + if ((await Promises.stat(p)).isFile()) { + const content = await Promises.readFile(p, 'utf-8'); + str += `${' '.repeat(indent)}${child}:\n`; + for (const line of content.split('\n')) { + str += `${' '.repeat(indent + 2)}${line}\n`; + } + } else { + str += `${' '.repeat(indent)}${child}/\n`; + await printDir(p, indent + 2); + } + } + }; + + await printDir(testDir, 0); + await assertSnapshot(str); + }; + + test('creates a snapshot', async () => { + const ctx = makeContext({ + file: 'foo/bar', + fullTitle: () => 'hello world!' + }); + + await ctx.assert({ cool: true }); + await snapshotFileTree(); + }); + + test('validates a snapshot', async () => { + const ctx1 = makeContext({ + file: 'foo/bar', + fullTitle: () => 'hello world!' + }); + + await ctx1.assert({ cool: true }); + + const ctx2 = makeContext({ + file: 'foo/bar', + fullTitle: () => 'hello world!' + }); + + // should pass: + await ctx2.assert({ cool: true }); + + const ctx3 = makeContext({ + file: 'foo/bar', + fullTitle: () => 'hello world!' + }); + + // should fail: + await assertThrowsAsync(() => ctx3.assert({ cool: false })); + }); + + test('cleans up old snapshots', async () => { + const ctx1 = makeContext({ + file: 'foo/bar', + fullTitle: () => 'hello world!' + }); + + await ctx1.assert({ cool: true }); + await ctx1.assert({ nifty: true }); + await ctx1.assert({ customName: 1 }, { name: 'thirdTest', extension: 'txt' }); + await ctx1.assert({ customName: 2 }, { name: 'fourthTest' }); + + await snapshotFileTree(); + + const ctx2 = makeContext({ + file: 'foo/bar', + fullTitle: () => 'hello world!' + }); + + await ctx2.assert({ cool: true }); + await ctx2.assert({ customName: 1 }, { name: 'thirdTest' }); + await ctx2.removeOldSnapshots(); + + await snapshotFileTree(); + }); + + test('formats object nicely', async () => { + const circular: any = {}; + circular.a = circular; + + await assertSnapshot([ + 1, + true, + undefined, + null, + 123n, + Symbol('heyo'), + 'hello', + { hello: 'world' }, + circular, + new Map([['hello', 1], ['goodbye', 2]]), + new Set([1, 2, 3]), + function helloWorld() { }, + /hello/g, + new Array(10).fill('long string'.repeat(10)), + { [Symbol.for('debug.description')]() { return `Range [1 -> 5]`; } }, + ]); + }); +}); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 3b823317303..4770e9ef0bd 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -15,7 +15,7 @@ import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort } from 'vs/base/node/ports'; import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; +import { buildHelpMessage, buildVersionMessage, NATIVE_CLI_COMMANDS, OPTIONS } from 'vs/platform/environment/node/argv'; import { addArg, parseCLIProcessArgv } from 'vs/platform/environment/node/argvHelper'; import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from 'vs/platform/environment/node/stdin'; import { createWaitMarkerFileSync } from 'vs/platform/environment/node/wait'; @@ -51,31 +51,33 @@ export async function main(argv: string[]): Promise { return; } - if (args.tunnel) { - if (!product.tunnelApplicationName) { - console.error(`'tunnel' command not supported in ${product.applicationName}`); - return; - } - const tunnelArgs = argv.slice(argv.indexOf('tunnel') + 1); // all arguments behind `tunnel` - return new Promise((resolve, reject) => { - let tunnelProcess: ChildProcess; - const stdio: StdioOptions = ['ignore', 'pipe', 'pipe']; - if (process.env['VSCODE_DEV']) { - tunnelProcess = spawn('cargo', ['run', '--', 'tunnel', ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio }); - } else { - const appPath = process.platform === 'darwin' - // ./Contents/MacOS/Electron => ./Contents/Resources/app/bin/code-tunnel-insiders - ? join(dirname(dirname(process.execPath)), 'Resources', 'app') - : dirname(process.execPath); - const tunnelCommand = join(appPath, 'bin', `${product.tunnelApplicationName}${isWindows ? '.exe' : ''}`); - tunnelProcess = spawn(tunnelCommand, ['tunnel', ...tunnelArgs], { cwd: cwd(), stdio }); + for (const subcommand of NATIVE_CLI_COMMANDS) { + if (args[subcommand]) { + if (!product.tunnelApplicationName) { + console.error(`'${subcommand}' command not supported in ${product.applicationName}`); + return; } + const tunnelArgs = argv.slice(argv.indexOf(subcommand) + 1); // all arguments behind `tunnel` + return new Promise((resolve, reject) => { + let tunnelProcess: ChildProcess; + const stdio: StdioOptions = ['ignore', 'pipe', 'pipe']; + if (process.env['VSCODE_DEV']) { + tunnelProcess = spawn('cargo', ['run', '--', subcommand, ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio }); + } else { + const appPath = process.platform === 'darwin' + // ./Contents/MacOS/Electron => ./Contents/Resources/app/bin/code-tunnel-insiders + ? join(dirname(dirname(process.execPath)), 'Resources', 'app') + : dirname(process.execPath); + const tunnelCommand = join(appPath, 'bin', `${product.tunnelApplicationName}${isWindows ? '.exe' : ''}`); + tunnelProcess = spawn(tunnelCommand, [subcommand, ...tunnelArgs], { cwd: cwd(), stdio }); + } - tunnelProcess.stdout!.pipe(process.stdout); - tunnelProcess.stderr!.pipe(process.stderr); - tunnelProcess.on('exit', resolve); - tunnelProcess.on('error', reject); - }); + tunnelProcess.stdout!.pipe(process.stdout); + tunnelProcess.stderr!.pipe(process.stderr); + tunnelProcess.on('exit', resolve); + tunnelProcess.on('error', reject); + }); + } } // Help diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index a4ffe3053b5..6757066169f 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -17,7 +17,6 @@ import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker import { DiffAlgorithmName, IDiffComputationResult, IEditorWorkerService, ILineChange, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { regExpFlags } from 'vs/base/common/strings'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; import { StopWatch } from 'vs/base/common/stopwatch'; @@ -582,7 +581,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien public async textualSuggest(resources: URI[], leadingWord: string | undefined, wordDefRegExp: RegExp): Promise<{ words: string[]; duration: number } | null> { const proxy = await this._withSyncedResources(resources); const wordDef = wordDefRegExp.source; - const wordDefFlags = regExpFlags(wordDefRegExp); + const wordDefFlags = wordDefRegExp.flags; return proxy.textualSuggest(resources.map(r => r.toString()), leadingWord, wordDef, wordDefFlags); } @@ -594,7 +593,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien } const wordDefRegExp = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition(); const wordDef = wordDefRegExp.source; - const wordDefFlags = regExpFlags(wordDefRegExp); + const wordDefFlags = wordDefRegExp.flags; return proxy.computeWordRanges(resource.toString(), range, wordDef, wordDefFlags); }); } @@ -607,7 +606,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien } const wordDefRegExp = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition(); const wordDef = wordDefRegExp.source; - const wordDefFlags = regExpFlags(wordDefRegExp); + const wordDefFlags = wordDefRegExp.flags; return proxy.navigateValueSet(resource.toString(), range, up, wordDef, wordDefFlags); }); } diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index 8fff7ae3155..fe686d3e441 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -20,8 +20,8 @@ } .mtkoverflow { - background-color: var(--vscode-button-background, --vscode-editor-background); - color: var(--vscode-button-foreground, --vscode-editor-foreground); + background-color: var(--vscode-button-background, var(--vscode-editor-background)); + color: var(--vscode-button-foreground, var(--vscode-editor-foreground)); border-width: 1px; border-style: solid; border-color: var(--vscode-contrastBorder); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/colors.ts b/src/vs/editor/browser/widget/diffEditorWidget2/colors.ts index 8c78445d74b..449e67f06f8 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/colors.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/colors.ts @@ -11,3 +11,9 @@ export const diffMoveBorder = registerColor( { dark: '#8b8b8b9c', light: '#8b8b8b9c', hcDark: '#8b8b8b9c', hcLight: '#8b8b8b9c', }, localize('diffEditor.move.border', 'The border color for text that got moved in the diff editor.') ); + +export const diffMoveBorderActive = registerColor( + 'diffEditor.moveActive.border', + { dark: '#FFA500', light: '#FFA500', hcDark: '#FFA500', hcLight: '#FFA500', }, + localize('diffEditor.moveActive.border', 'The active border color for text that got moved in the diff editor.') +); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts index 5d9af4098f9..194f37c9882 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts @@ -5,14 +5,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, derived } from 'vs/base/common/observable'; -import { isDefined } from 'vs/base/common/types'; import { arrowRevertChange, diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackground, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackground, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditorWidget2/decorations'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel'; import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines'; import { applyObservableDecorations } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; -import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; @@ -42,46 +40,37 @@ export class DiffEditorDecorations extends Disposable { const originalDecorations: IModelDeltaDecoration[] = []; const modifiedDecorations: IModelDeltaDecoration[] = []; - for (const m of diff.mappings) { - const fullRangeOriginal = LineRange.subtract(m.lineRangeMapping.originalRange, currentMove?.lineRangeMapping.original) - .map(i => i.toInclusiveRange()).filter(isDefined); - for (const range of fullRangeOriginal) { - originalDecorations.push({ range, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground }); - } - - const fullRangeModified = LineRange.subtract(m.lineRangeMapping.modifiedRange, currentMove?.lineRangeMapping.modified) - .map(i => i.toInclusiveRange()).filter(isDefined); - for (const range of fullRangeModified) { - modifiedDecorations.push({ range, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground }); - } - - if (m.lineRangeMapping.modifiedRange.isEmpty || m.lineRangeMapping.originalRange.isEmpty) { - for (const range of fullRangeOriginal) { - originalDecorations.push({ range, options: diffWholeLineDeleteDecoration }); + if (!currentMove) { + for (const m of diff.mappings) { + if (!m.lineRangeMapping.originalRange.isEmpty) { + originalDecorations.push({ range: m.lineRangeMapping.originalRange.toInclusiveRange()!, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground }); } - for (const range of fullRangeModified) { - modifiedDecorations.push({ range, options: diffWholeLineAddDecoration }); + if (!m.lineRangeMapping.modifiedRange.isEmpty) { + modifiedDecorations.push({ range: m.lineRangeMapping.modifiedRange.toInclusiveRange()!, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground }); } - } else { - for (const i of m.lineRangeMapping.innerChanges || []) { - if (currentMove - && (currentMove.lineRangeMapping.original.intersect(new LineRange(i.originalRange.startLineNumber, i.originalRange.endLineNumber)) - || currentMove.lineRangeMapping.modified.intersect(new LineRange(i.modifiedRange.startLineNumber, i.modifiedRange.endLineNumber)))) { - continue; - } - // Don't show empty markers outside the line range - if (m.lineRangeMapping.originalRange.contains(i.originalRange.startLineNumber)) { - originalDecorations.push({ range: i.originalRange, options: (i.originalRange.isEmpty() && showEmptyDecorations) ? diffDeleteDecorationEmpty : diffDeleteDecoration }); + if (m.lineRangeMapping.modifiedRange.isEmpty || m.lineRangeMapping.originalRange.isEmpty) { + if (!m.lineRangeMapping.originalRange.isEmpty) { + originalDecorations.push({ range: m.lineRangeMapping.originalRange.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); } - if (m.lineRangeMapping.modifiedRange.contains(i.modifiedRange.startLineNumber)) { - modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations) ? diffAddDecorationEmpty : diffAddDecoration }); + if (!m.lineRangeMapping.modifiedRange.isEmpty) { + modifiedDecorations.push({ range: m.lineRangeMapping.modifiedRange.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.lineRangeMapping.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.lineRangeMapping.originalRange.contains(i.originalRange.startLineNumber)) { + originalDecorations.push({ range: i.originalRange, options: (i.originalRange.isEmpty() && showEmptyDecorations) ? diffDeleteDecorationEmpty : diffDeleteDecoration }); + } + if (m.lineRangeMapping.modifiedRange.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations) ? diffAddDecorationEmpty : diffAddDecoration }); + } } } - } - if (!m.lineRangeMapping.modifiedRange.isEmpty && this._options.shouldRenderRevertArrows.read(reader) && !currentMove) { - modifiedDecorations.push({ range: Range.fromPositions(new Position(m.lineRangeMapping.modifiedRange.startLineNumber, 1)), options: arrowRevertChange }); + if (!m.lineRangeMapping.modifiedRange.isEmpty && this._options.shouldRenderRevertArrows.read(reader) && !currentMove) { + modifiedDecorations.push({ range: Range.fromPositions(new Position(m.lineRangeMapping.modifiedRange.startLineNumber, 1)), options: arrowRevertChange }); + } } } @@ -107,7 +96,7 @@ export class DiffEditorDecorations extends Disposable { originalDecorations.push({ range: m.lineRangeMapping.original.toInclusiveRange()!, options: { description: 'moved', - blockClassName: 'movedOriginal', + blockClassName: 'movedOriginal' + (m === currentMove ? ' currentMove' : ''), blockPadding: [MovedBlocksLinesPart.movedCodeBlockPadding, 0, MovedBlocksLinesPart.movedCodeBlockPadding, MovedBlocksLinesPart.movedCodeBlockPadding], } }); @@ -115,7 +104,7 @@ export class DiffEditorDecorations extends Disposable { modifiedDecorations.push({ range: m.lineRangeMapping.modified.toInclusiveRange()!, options: { description: 'moved', - blockClassName: 'movedModified', + blockClassName: 'movedModified' + (m === currentMove ? ' currentMove' : ''), blockPadding: [4, 0, 4, 4], } }); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts index cce99a94271..ee764c81aed 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts @@ -35,8 +35,8 @@ export class DiffEditorEditors extends Disposable { ) { super(); - this.original = this._createLeftHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.originalEditor || {}); - this.modified = this._createRightHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.modifiedEditor || {}); + this.original = this._register(this._createLeftHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.originalEditor || {})); + this.modified = this._register(this._createRightHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.modifiedEditor || {})); this._register(autorunHandleChanges({ createEmptyChangeSummary: () => ({} as IDiffEditorConstructionOptions), diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts index 4833d3de651..4927ebeb9c1 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts @@ -11,13 +11,14 @@ import { ISerializedLineRange, LineRange } from 'vs/editor/common/core/lineRange import { Range } from 'vs/editor/common/core/range'; import { IDocumentDiff, IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider'; import { LineRangeMapping, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; -import { lineRangeMappingFromRangeMappings } from 'vs/editor/common/diff/standardLinesDiffComputer'; +import { StandardLinesDiffComputer, lineRangeMappingFromRangeMappings } from 'vs/editor/common/diff/standardLinesDiffComputer'; import { IDiffEditorModel, IDiffEditorViewModel } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper'; import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos'; import { lengthAdd, lengthDiffNonNegative, lengthGetLineCount, lengthOfRange, lengthToPosition, lengthZero, positionToLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; import { DiffEditorOptions } from './diffEditorOptions'; +import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; export class DiffEditorViewModel extends Disposable implements IDiffEditorViewModel { private readonly _isDiffUpToDate = observableValue('isDiffUpToDate', false); @@ -105,40 +106,36 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo this._register(model.modified.onDidChangeContent((e) => { const diff = this._diff.get(); - if (!diff) { - return; - } - - const textEdits = TextEditInfo.fromModelContentChanges(e.changes); - const result = applyModifiedEdits(this._lastDiff!, textEdits, model.original, model.modified); - if (result) { - this._lastDiff = result; - transaction(tx => { - this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); - updateUnchangedRegions(result, tx); - const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); - }); + if (diff) { + const textEdits = TextEditInfo.fromModelContentChanges(e.changes); + const result = applyModifiedEdits(this._lastDiff!, textEdits, model.original, model.modified); + if (result) { + this._lastDiff = result; + transaction(tx => { + this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); + updateUnchangedRegions(result, tx); + const currentSyncedMovedText = this.syncedMovedTexts.get(); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); + }); + } } debouncer.schedule(); })); this._register(model.original.onDidChangeContent((e) => { const diff = this._diff.get(); - if (!diff) { - return; - } - - const textEdits = TextEditInfo.fromModelContentChanges(e.changes); - const result = applyOriginalEdits(this._lastDiff!, textEdits, model.original, model.modified); - if (result) { - this._lastDiff = result; - transaction(tx => { - this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); - updateUnchangedRegions(result, tx); - const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); - }); + if (diff) { + const textEdits = TextEditInfo.fromModelContentChanges(e.changes); + const result = applyOriginalEdits(this._lastDiff!, textEdits, model.original, model.modified); + if (result) { + this._lastDiff = result; + transaction(tx => { + this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); + updateUnchangedRegions(result, tx); + const currentSyncedMovedText = this.syncedMovedTexts.get(); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); + }); + } } debouncer.schedule(); @@ -151,6 +148,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo debouncer.cancel(); contentChangedSignal.read(reader); documentDiffProviderOptionChanged.read(reader); + readHotReloadableExport(StandardLinesDiffComputer, reader); this._isDiffUpToDate.set(false, undefined); @@ -175,7 +173,6 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified) ?? result; result = applyModifiedEdits(result, modifiedTextEditInfos, model.original, model.modified) ?? result; - transaction(tx => { updateUnchangedRegions(result, tx); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts index d312eb5629a..13c654d867f 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts @@ -91,14 +91,10 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { ) }); -/* -TODO@hediet add this back once move detection is more polished. -Users can still enable this via settings.json (config.diffEditor.experimental.showMoves). - MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: new ToggleShowMovedCodeBlocks().desc.id, - title: localize('showMoves', "Show Moves"), + title: localize('showMoves', "Show Moved Code Blocks"), icon: Codicon.move, toggled: ContextKeyEqualsExpr.create('config.diffEditor.experimental.showMoves', true), }, @@ -106,7 +102,6 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { group: '1_diff', when: ContextKeyEqualsExpr.create('diffEditorVersion', 2) }); -*/ const diffEditorCategory: ILocalizedString = { value: localize('diffEditor', 'Diff Editor'), diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts index 21b5f67aaae..63973ae2e03 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts @@ -79,6 +79,8 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { private readonly _options: DiffEditorOptions; private readonly _editors: DiffEditorEditors; + private readonly movedBlocksLinesPart = observableValue('MovedBlocksLinesPart', undefined); + constructor( private readonly _domElement: HTMLElement, options: Readonly, @@ -204,13 +206,15 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._register(keepAlive(this._layoutInfo, true)); - this._register(new MovedBlocksLinesPart( - this.elements.root, - this._diffModel, - this._layoutInfo.map(i => i.originalEditor), - this._layoutInfo.map(i => i.modifiedEditor), - this._editors, - )); + this._register(autorunWithStore((reader, store) => { + this.movedBlocksLinesPart.set(store.add(new (readHotReloadableExport(MovedBlocksLinesPart, reader))( + this.elements.root, + this._diffModel, + this._layoutInfo.map(i => i.originalEditor), + this._layoutInfo.map(i => i.modifiedEditor), + this._editors, + )), undefined); + })); this._register(applyStyle(this.elements.overlay, { width: this._layoutInfo.map((i, r) => i.originalEditor.width + (this._options.renderSideBySide.read(r) ? 0 : i.modifiedEditor.width)), @@ -285,13 +289,15 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { const originalWidth = sashLeft ?? Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft); const modifiedWidth = width - originalWidth - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); - this.elements.original.style.width = originalWidth + 'px'; + const movedBlocksLinesWidth = this.movedBlocksLinesPart.read(reader)?.width.read(reader) ?? 0; + const originalWidthWithoutMovedBlockLines = originalWidth - movedBlocksLinesWidth; + this.elements.original.style.width = originalWidthWithoutMovedBlockLines + 'px'; this.elements.original.style.left = '0px'; this.elements.modified.style.width = modifiedWidth + 'px'; this.elements.modified.style.left = originalWidth + 'px'; - this._editors.original.layout({ width: originalWidth, height }); + this._editors.original.layout({ width: originalWidthWithoutMovedBlockLines, height }); this._editors.modified.layout({ width: modifiedWidth, height }); return { diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts index 92fb780d62c..3a95ce28942 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts @@ -265,7 +265,7 @@ export class ViewZoneManager extends Disposable { } else { const delta = a.modifiedHeightInPx - a.originalHeightInPx; if (delta > 0) { - if (syncedMovedText?.lineRangeMapping.original.contains(a.originalRange.endLineNumberExclusive - 1)) { + if (syncedMovedText?.lineRangeMapping.original.delta(-1).deltaLength(2).contains(a.originalRange.endLineNumberExclusive - 1)) { continue; } @@ -276,7 +276,7 @@ export class ViewZoneManager extends Disposable { showInHiddenAreas: true, }); } else { - if (syncedMovedText?.lineRangeMapping.modified.contains(a.modifiedRange.endLineNumberExclusive - 1)) { + if (syncedMovedText?.lineRangeMapping.modified.delta(-1).deltaLength(2).contains(a.modifiedRange.endLineNumberExclusive - 1)) { continue; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts b/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts index b4b628c711f..4453cb7ce2d 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts @@ -3,17 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, observableFromEvent, observableSignalFromEvent } from 'vs/base/common/observable'; +import { booleanComparator, compareBy, findMaxIdxBy, numberComparator, tieBreakComparators } from 'vs/base/common/arrays'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, autorun, derived, keepAlive, observableFromEvent, observableSignalFromEvent, observableValue } from 'vs/base/common/observable'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel'; import { EditorLayoutInfo } from 'vs/editor/common/config/editorOptions'; import { LineRange } from 'vs/editor/common/core/lineRange'; +import { OffsetRange, OffsetRangeSet } from 'vs/editor/common/core/offsetRange'; export class MovedBlocksLinesPart extends Disposable { public static readonly movedCodeBlockPadding = 4; + private readonly _element: SVGElement; + private readonly _originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); + private readonly _modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); + private readonly _viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); + + public readonly width = observableValue('width', 0); + constructor( private readonly _rootElement: HTMLElement, private readonly _diffModel: IObservable, @@ -23,9 +32,10 @@ export class MovedBlocksLinesPart extends Disposable { ) { super(); - const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - element.setAttribute('class', 'moved-blocks-lines'); - this._rootElement.appendChild(element); + this._element = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this._element.setAttribute('class', 'moved-blocks-lines'); + this._rootElement.appendChild(this._element); + this._register(toDisposable(() => this._element.remove())); this._register(autorun(reader => { /** @description update moved blocks lines positioning */ @@ -35,58 +45,142 @@ export class MovedBlocksLinesPart extends Disposable { return; } - element.style.left = `${info.width - info.verticalScrollbarWidth}px`; - element.style.height = `${info.height}px`; - element.style.width = `${info.verticalScrollbarWidth + info.contentLeft - MovedBlocksLinesPart.movedCodeBlockPadding}px`; + this._element.style.left = `${info.width - info.verticalScrollbarWidth}px`; + this._element.style.height = `${info.height}px`; + this._element.style.width = `${info.verticalScrollbarWidth + info.contentLeft - MovedBlocksLinesPart.movedCodeBlockPadding + this.width.read(reader)}px`; })); - const originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); - const modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); - const viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); + this._register(keepAlive(this._state, true)); + } - this._register(autorun(reader => { - element.replaceChildren(); + private readonly _state = derived(reader => { + /** @description update moved blocks lines */ - /** @description update moved blocks lines */ - const moves = this._diffModel.read(reader)?.diff.read(reader)?.movedTexts; - if (!moves) { - return; + this._element.replaceChildren(); + const model = this._diffModel.read(reader); + const moves = model?.diff.read(reader)?.movedTexts; + if (!moves || moves.length === 0) { + this.width.set(0, undefined); + return; + } + + this._viewZonesChanged.read(reader); + + const infoOrig = this._originalEditorLayoutInfo.read(reader); + const infoMod = this._modifiedEditorLayoutInfo.read(reader); + if (!infoOrig || !infoMod) { + this.width.set(0, undefined); + return; + } + + const lines = moves.map((move) => { + function computeLineStart(range: LineRange, editor: ICodeEditor) { + const t1 = editor.getTopForLineNumber(range.startLineNumber); + const t2 = editor.getTopForLineNumber(range.endLineNumberExclusive); + return (t1 + t2) / 2; } - viewZonesChanged.read(reader); + const start = computeLineStart(move.lineRangeMapping.original, this._editors.original); + const startOffset = this._originalScrollTop.read(reader); + const end = computeLineStart(move.lineRangeMapping.modified, this._editors.modified); + const endOffset = this._modifiedScrollTop.read(reader); - const info = this._originalEditorLayoutInfo.read(reader); - const info2 = this._modifiedEditorLayoutInfo.read(reader); - if (!info || !info2) { - return; + const from = start - startOffset; + const to = end - endOffset; + + const top = Math.min(start, end); + const bottom = Math.max(start, end); + + return { range: new OffsetRange(top, bottom), from, to, fromWithoutScroll: start, toWithoutScroll: end, move }; + }); + + lines.sort(tieBreakComparators( + compareBy(l => l.fromWithoutScroll > l.toWithoutScroll, booleanComparator), + compareBy(l => -l.fromWithoutScroll, numberComparator) + )); + + const layout = LinesLayout.compute(lines.map(l => l.range)); + + const padding = 10; + const lineAreaLeft = infoOrig.verticalScrollbarWidth; + const lineAreaWidth = (layout.getTrackCount() - 1) * 10 + padding * 2; + const width = lineAreaLeft + lineAreaWidth + (infoMod.contentLeft - MovedBlocksLinesPart.movedCodeBlockPadding); + + let idx = 0; + for (const line of lines) { + const track = layout.getTrack(idx); + const verticalY = lineAreaLeft + padding + track * 10; + + const arrowHeight = 15; + const arrowWidth = 15; + const right = width; + + const rectWidth = infoMod.glyphMarginWidth + infoMod.lineNumbersWidth; + const rectHeight = 18; + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.classList.add('arrow-rectangle'); + rect.setAttribute('x', `${right - rectWidth}`); + rect.setAttribute('y', `${line.to - rectHeight / 2}`); + rect.setAttribute('width', `${rectWidth}`); + rect.setAttribute('height', `${rectHeight}`); + this._element.appendChild(rect); + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + if (line.move === model.syncedMovedTexts.read(reader)) { + path.classList.add('currentMove'); } - const width = info.verticalScrollbarWidth + info.contentLeft - MovedBlocksLinesPart.movedCodeBlockPadding; + path.setAttribute('d', `M ${0} ${line.from} L ${verticalY} ${line.from} L ${verticalY} ${line.to} L ${right - arrowWidth} ${line.to}`); + path.setAttribute('fill', 'none'); + this._element.appendChild(path); - let idx = 0; - for (const m of moves) { - function computeLineStart(range: LineRange, editor: ICodeEditor) { - const t1 = editor.getTopForLineNumber(range.startLineNumber); - const t2 = editor.getTopForLineNumber(range.endLineNumberExclusive); - return (t1 + t2) / 2; + const arrowRight = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); + arrowRight.classList.add('arrow'); + if (line.move === model.syncedMovedTexts.read(reader)) { + arrowRight.classList.add('currentMove'); + } + arrowRight.setAttribute('points', `${right - arrowWidth},${line.to - arrowHeight / 2} ${right},${line.to} ${right - arrowWidth},${line.to + arrowHeight / 2}`); + this._element.appendChild(arrowRight); + + idx++; + } + + this.width.set(lineAreaWidth, undefined); + }); +} + +class LinesLayout { + public static compute(lines: OffsetRange[]): LinesLayout { + const setsPerTrack: OffsetRangeSet[] = []; + const trackPerLineIdx: number[] = []; + + for (const line of lines) { + let trackIdx = setsPerTrack.findIndex(set => !set.intersectsStrict(line)); + if (trackIdx === -1) { + const maxTrackCount = 6; + if (setsPerTrack.length >= maxTrackCount) { + trackIdx = findMaxIdxBy(setsPerTrack, compareBy(set => set.intersectWithRangeLength(line), numberComparator)); + } else { + trackIdx = setsPerTrack.length; + setsPerTrack.push(new OffsetRangeSet()); } - - const start = computeLineStart(m.lineRangeMapping.original, this._editors.original); - const startOffset = originalScrollTop.read(reader); - const end = computeLineStart(m.lineRangeMapping.modified, this._editors.modified); - const endOffset = modifiedScrollTop.read(reader); - - const top = start - startOffset; - const bottom = end - endOffset; - - const center = (width / 2) - moves.length * 5 + idx * 10; - idx++; - - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('d', `M ${0} ${top} L ${center} ${top} L ${center} ${bottom} L ${width} ${bottom}`); - - path.setAttribute('fill', 'none'); - element.appendChild(path); } - })); + setsPerTrack[trackIdx].addRange(line); + trackPerLineIdx.push(trackIdx); + } + + return new LinesLayout(setsPerTrack.length, trackPerLineIdx); + } + + private constructor( + private readonly _trackCount: number, + private readonly trackPerLineIdx: number[] + ) { } + + getTrack(lineIdx: number): number { + return this.trackPerLineIdx[lineIdx]; + } + + getTrackCount(): number { + return this._trackCount; } } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/style.css b/src/vs/editor/browser/widget/diffEditorWidget2/style.css index f8243853b70..8fd6377362f 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/style.css +++ b/src/vs/editor/browser/widget/diffEditorWidget2/style.css @@ -78,6 +78,26 @@ border: 2px solid var(--vscode-diffEditor-move-border); } +.monaco-editor .movedOriginal.currentMove, .monaco-editor .movedModified.currentMove { + border: 2px solid var(--vscode-diffEditor-moveActive-border); +} + +.monaco-diff-editor .moved-blocks-lines path.currentMove { + stroke: var(--vscode-diffEditor-moveActive-border); +} + +.monaco-diff-editor .moved-blocks-lines .arrow { + fill: var(--vscode-diffEditor-move-border); +} + +.monaco-diff-editor .moved-blocks-lines .arrow.currentMove { + fill: var(--vscode-diffEditor-moveActive-border); +} + +.monaco-diff-editor .moved-blocks-lines .arrow-rectangle { + fill: var(--vscode-editor-background); +} + .monaco-diff-editor .moved-blocks-lines { position: absolute; pointer-events: none; diff --git a/src/vs/editor/browser/widget/media/diffEditor.css b/src/vs/editor/browser/widget/media/diffEditor.css index 66fa2b5ab83..6d086fe1031 100644 --- a/src/vs/editor/browser/widget/media/diffEditor.css +++ b/src/vs/editor/browser/widget/media/diffEditor.css @@ -81,7 +81,7 @@ } .monaco-editor .line-insert, .monaco-diff-editor .line-insert { - background-color: var(--vscode-diffEditor-insertedLineBackground, --vscode-diffEditor-insertedTextBackground); + background-color: var(--vscode-diffEditor-insertedLineBackground, var(--vscode-diffEditor-insertedTextBackground)); } .monaco-editor .line-insert, @@ -106,7 +106,7 @@ .monaco-editor .inline-added-margin-view-zone, .monaco-editor .gutter-insert, .monaco-diff-editor .gutter-insert { - background-color: var(--vscode-diffEditorGutter-insertedLineBackground, --vscode-diffEditor-insertedLineBackground, --vscode-diffEditor-insertedTextBackground); + background-color: var(--vscode-diffEditorGutter-insertedLineBackground, var(--vscode-diffEditor-insertedLineBackground), var(--vscode-diffEditor-insertedTextBackground)); } .monaco-editor .char-delete, .monaco-diff-editor .char-delete { @@ -114,12 +114,12 @@ } .monaco-editor .line-delete, .monaco-diff-editor .line-delete { - background-color: var(--vscode-diffEditor-removedLineBackground, --vscode-diffEditor-removedTextBackground); + background-color: var(--vscode-diffEditor-removedLineBackground, var(--vscode-diffEditor-removedTextBackground)); } .monaco-editor .inline-deleted-margin-view-zone, .monaco-editor .gutter-delete, .monaco-diff-editor .gutter-delete { - background-color: var(--vscode-diffEditorGutter-removedLineBackground, --vscode-diffEditor-removedLineBackground, --vscode-diffEditor-removedTextBackground); + background-color: var(--vscode-diffEditorGutter-removedLineBackground, var(--vscode-diffEditor-removedLineBackground), var(--vscode-diffEditor-removedTextBackground)); } .monaco-diff-editor.side-by-side .editor.modified { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 6895706d518..eee7baf03c6 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -730,6 +730,11 @@ export interface IEditorOptions { * Controls whether the editor receives tabs or defers them to the workbench for navigation. */ tabFocusMode?: boolean; + + /** + * Controls whether the accessibility hint should be provided to screen reader users when an inline completion is shown. + */ + inlineCompletionsAccessibilityVerbose?: boolean; } /** @@ -5143,7 +5148,8 @@ export const enum EditorOption { layoutInfo, wrappingInfo, defaultColorDecorators, - colorDecoratorsActivatedOn + colorDecoratorsActivatedOn, + inlineCompletionsAccessibilityVerbose } export const EditorOptions = { @@ -5734,6 +5740,8 @@ export const EditorOptions = { )), suggest: register(new EditorSuggest()), inlineSuggest: register(new InlineEditorSuggest()), + inlineCompletionsAccessibilityVerbose: register(new EditorBooleanOption(EditorOption.inlineCompletionsAccessibilityVerbose, 'inlineCompletionsAccessibilityVerbose', false, + { description: nls.localize('inlineCompletionsAccessibilityVerbose', "Controls whether the accessibility hint should be provided to screen reader users when an inline completion is shown.") })), suggestFontSize: register(new EditorIntOption( EditorOption.suggestFontSize, 'suggestFontSize', 0, 0, 1000, diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index c8779a2881a..70acb0476d6 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/src/vs/editor/common/core/lineRange.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from 'vs/base/common/errors'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Range } from 'vs/editor/common/core/range'; /** @@ -154,6 +155,10 @@ export class LineRange { return new LineRange(this.startLineNumber + offset, this.endLineNumberExclusive + offset); } + public deltaLength(offset: number): LineRange { + return new LineRange(this.startLineNumber, this.endLineNumberExclusive + offset); + } + /** * The number of lines this line range spans. */ @@ -235,6 +240,14 @@ export class LineRange { public includes(lineNumber: number): boolean { return this.startLineNumber <= lineNumber && lineNumber < this.endLineNumberExclusive; } + + /** + * Converts this 1-based line range to a 0-based offset range (subtracts 1!). + * @internal + */ + public toOffsetRange(): OffsetRange { + return new OffsetRange(this.startLineNumber - 1, this.endLineNumberExclusive - 1); + } } export type ISerializedLineRange = [startLineNumber: number, endLineNumberExclusive: number]; diff --git a/src/vs/editor/common/core/offsetRange.ts b/src/vs/editor/common/core/offsetRange.ts index 14ff2039fc7..d4129b1c532 100644 --- a/src/vs/editor/common/core/offsetRange.ts +++ b/src/vs/editor/common/core/offsetRange.ts @@ -48,6 +48,14 @@ export class OffsetRange { return new OffsetRange(this.start + offset, this.endExclusive + offset); } + public deltaStart(offset: number): OffsetRange { + return new OffsetRange(this.start + offset, this.endExclusive); + } + + public deltaEnd(offset: number): OffsetRange { + return new OffsetRange(this.start, this.endExclusive + offset); + } + public get length(): number { return this.endExclusive - this.start; } @@ -90,4 +98,66 @@ export class OffsetRange { } return undefined; } + + public slice(arr: T[]): T[] { + return arr.slice(this.start, this.endExclusive); + } +} + +export class OffsetRangeSet { + private readonly _sortedRanges: OffsetRange[] = []; + + public addRange(range: OffsetRange): void { + let i = 0; + while (i < this._sortedRanges.length && this._sortedRanges[i].endExclusive < range.start) { + i++; + } + let j = i; + while (j < this._sortedRanges.length && this._sortedRanges[j].start <= range.endExclusive) { + j++; + } + if (i === j) { + this._sortedRanges.splice(i, 0, range); + } else { + const start = Math.min(range.start, this._sortedRanges[i].start); + const end = Math.max(range.endExclusive, this._sortedRanges[j - 1].endExclusive); + this._sortedRanges.splice(i, j - i, new OffsetRange(start, end)); + } + } + + public toString(): string { + return this._sortedRanges.map(r => r.toString()).join(', '); + } + + /** + * Returns of there is a value that is contained in this instance and the given range. + */ + public intersectsStrict(other: OffsetRange): boolean { + // TODO use binary search + let i = 0; + while (i < this._sortedRanges.length && this._sortedRanges[i].endExclusive <= other.start) { + i++; + } + return i < this._sortedRanges.length && this._sortedRanges[i].start < other.endExclusive; + } + + public intersectWithRange(other: OffsetRange): OffsetRangeSet { + // TODO use binary search + slice + const result = new OffsetRangeSet(); + for (const range of this._sortedRanges) { + const intersection = range.intersect(other); + if (intersection) { + result.addRange(intersection); + } + } + return result; + } + + public intersectWithRangeLength(other: OffsetRange): number { + return this.intersectWithRange(other).length; + } + + public get length(): number { + return this._sortedRanges.reduce((prev, cur) => prev + cur.length, 0); + } } diff --git a/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts b/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts index b76c82d371b..c79d557e409 100644 --- a/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts +++ b/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts @@ -149,6 +149,29 @@ export function removeRandomMatches(sequence1: LinesSliceCharSequence, sequence2 diffs = result; } while (counter++ < 10 && shouldRepeat); + // Remove short suffixes/prefixes + for (let i = 0; i < diffs.length; i++) { + const cur = diffs[i]; + + let range1 = cur.seq1Range; + let range2 = cur.seq2Range; + + const fullRange1 = sequence1.extendToFullLines(cur.seq1Range); + const prefix = sequence1.getText(new OffsetRange(fullRange1.start, cur.seq1Range.start)); + if (prefix.length > 0 && prefix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 100) { + range1 = cur.seq1Range.deltaStart(-prefix.length); + range2 = cur.seq2Range.deltaStart(-prefix.length); + } + + const suffix = sequence1.getText(new OffsetRange(cur.seq1Range.endExclusive, fullRange1.endExclusive)); + if (suffix.length > 0 && (suffix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 150)) { + range1 = range1.deltaEnd(suffix.length); + range2 = range2.deltaEnd(suffix.length); + } + + diffs[i] = new SequenceDiff(range1, range2); + } + return diffs; } diff --git a/src/vs/editor/common/diff/linesDiffComputer.ts b/src/vs/editor/common/diff/linesDiffComputer.ts index 84505ab563a..d10888cb93f 100644 --- a/src/vs/editor/common/diff/linesDiffComputer.ts +++ b/src/vs/editor/common/diff/linesDiffComputer.ts @@ -155,6 +155,13 @@ export class SimpleLineRangeMapping { public flip(): SimpleLineRangeMapping { return new SimpleLineRangeMapping(this.modified, this.original); } + + public join(other: SimpleLineRangeMapping): SimpleLineRangeMapping { + return new SimpleLineRangeMapping( + this.original.join(other.original), + this.modified.join(other.modified), + ); + } } export class MovedText { diff --git a/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/src/vs/editor/common/diff/standardLinesDiffComputer.ts index b9ef58b85c2..20279a9fe68 100644 --- a/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -177,7 +177,7 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } private computeMoves(changes: LineRangeMapping[], originalLines: string[], modifiedLines: string[], hashedOriginalLines: number[], hashedModifiedLines: number[], timeout: ITimeout, considerWhitespaceChanges: boolean): MovedText[] { - const moves: MovedText[] = []; + const moves: SimpleLineRangeMapping[] = []; const deletions = changes .filter(c => c.modifiedRange.isEmpty && c.originalRange.length >= 3) .map(d => new LineRangeFragment(d.originalRange, originalLines, d)); @@ -199,17 +199,15 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } if (highestSimilarity > 0.90 && best) { - const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( - new OffsetRange(deletion.range.startLineNumber - 1, deletion.range.endLineNumberExclusive - 1), - new OffsetRange(best.range.startLineNumber - 1, best.range.endLineNumberExclusive - 1) - ), timeout, considerWhitespaceChanges); - const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); - insertions.delete(best); - moves.push(new MovedText(new SimpleLineRangeMapping(deletion.range, best.range), mappings)); + moves.push(new SimpleLineRangeMapping(deletion.range, best.range)); excludedChanges.add(deletion.source); excludedChanges.add(best.source); } + + if (!timeout.isValid()) { + return []; + } } const original3LineHashes = new SetMap(); @@ -242,9 +240,6 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { let lastMappings: PossibleMapping[] = []; for (let i = change.modifiedRange.startLineNumber; i < change.modifiedRange.endLineNumberExclusive - 2; i++) { const key = `${hashedModifiedLines[i - 1]}:${hashedModifiedLines[i + 1 - 1]}:${hashedModifiedLines[i + 2 - 1]}`; - - //const isWeakKey = (originalLines[i].trim().length + originalLines[i + 1].trim().length + originalLines[i + 2].trim().length < 20); - const currentModifiedRange = new LineRange(i, i + 3); const nextMappings: PossibleMapping[] = []; @@ -269,6 +264,10 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { }); lastMappings = nextMappings; } + + if (!timeout.isValid()) { + return []; + } } possibleMappings.sort(reverseOrder(compareBy(m => m.modifiedLineRange.length, numberComparator))); @@ -291,19 +290,49 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { const modifiedLineRange = s; const originalLineRange = s.delta(-diffOrigToMod); - const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( - new OffsetRange(originalLineRange.startLineNumber - 1, originalLineRange.endLineNumberExclusive - 1), - new OffsetRange(modifiedLineRange.startLineNumber - 1, modifiedLineRange.endLineNumberExclusive - 1) - ), timeout, considerWhitespaceChanges); - const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); - moves.push(new MovedText(new SimpleLineRangeMapping(originalLineRange, modifiedLineRange), mappings)); + moves.push(new SimpleLineRangeMapping(originalLineRange, modifiedLineRange)); modifiedSet.addRange(modifiedLineRange); originalSet.addRange(originalLineRange); } } - return moves; + // join moves + moves.sort(compareBy(m => m.original.startLineNumber, numberComparator)); + if (moves.length === 0) { + return []; + } + const joinedMoves = [moves[0]]; + for (let i = 1; i < moves.length; i++) { + const last = joinedMoves[joinedMoves.length - 1]; + const current = moves[i]; + + const originalDist = current.original.startLineNumber - last.original.endLineNumberExclusive; + const modifiedDist = current.modified.startLineNumber - last.modified.endLineNumberExclusive; + const currentMoveAfterLast = originalDist >= 0 && modifiedDist >= 0; + + if (currentMoveAfterLast && originalDist <= 1 && modifiedDist <= 1) { + joinedMoves[joinedMoves.length - 1] = last.join(current); + continue; + } + + const originalText = current.original.toOffsetRange().slice(originalLines).map(l => l.trim()).join('\n'); + if (originalText.length <= 10) { + // Ignore small moves + continue; + } + joinedMoves.push(current); + } + + const fullMoves = joinedMoves.map(m => { + const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( + m.original.toOffsetRange(), + m.modified.toOffsetRange(), + ), timeout, considerWhitespaceChanges); + const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); + return new MovedText(m, mappings); + }); + return fullMoves; } private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { @@ -659,7 +688,7 @@ export class LinesSliceCharSequence implements ISequence { private readonly firstCharOffsetByLineMinusOne: number[] = []; public readonly lineRange: OffsetRange; // To account for trimming - private readonly offsetByLine: number[] = []; + private readonly additionalOffsetByLine: number[] = []; constructor(public readonly lines: string[], lineRange: OffsetRange, public readonly considerWhitespaceChanges: boolean) { // This slice has to have lineRange.length many \n! (otherwise diffing against an empty slice will be problematic) @@ -687,7 +716,7 @@ export class LinesSliceCharSequence implements ISequence { line = trimmedStartLine.trimEnd(); } - this.offsetByLine.push(offset); + this.additionalOffsetByLine.push(offset); for (let i = 0; i < line.length; i++) { this.elements.push(line.charCodeAt(i)); @@ -700,7 +729,7 @@ export class LinesSliceCharSequence implements ISequence { } } // To account for the last line - this.offsetByLine.push(0); + this.additionalOffsetByLine.push(0); } toString() { @@ -767,7 +796,7 @@ export class LinesSliceCharSequence implements ISequence { } const offsetOfFirstCharInLine = i === 0 ? 0 : this.firstCharOffsetByLineMinusOne[i - 1]; - return new Position(this.lineRange.start + i + 1, offset - offsetOfFirstCharInLine + 1 + this.offsetByLine[i]); + return new Position(this.lineRange.start + i + 1, offset - offsetOfFirstCharInLine + 1 + this.additionalOffsetByLine[i]); } public translateRange(range: OffsetRange): Range { @@ -805,9 +834,52 @@ export class LinesSliceCharSequence implements ISequence { return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber; } - isStronglyEqual(offset1: number, offset2: number): boolean { + public isStronglyEqual(offset1: number, offset2: number): boolean { return this.elements[offset1] === this.elements[offset2]; } + + public extendToFullLines(range: OffsetRange): OffsetRange { + const firstIdx = findLastIdxMonotonous(this.firstCharOffsetByLineMinusOne, x => x <= range.start); + const lastIdx = findFirstIdxMonotonous(this.firstCharOffsetByLineMinusOne, x => range.endExclusive <= x); + + const start = firstIdx === -1 ? 0 : this.firstCharOffsetByLineMinusOne[firstIdx]; + const end = lastIdx === this.firstCharOffsetByLineMinusOne.length ? this.elements.length : this.firstCharOffsetByLineMinusOne[lastIdx]; + return new OffsetRange(start, end); + } +} + +/** + * @returns -1 if predicate is false for all items + */ +function findLastIdxMonotonous(arr: T[], predicate: (item: T) => boolean): number { + let i = 0; + let j = arr.length; + while (i < j) { + const k = Math.floor((i + j) / 2); + if (predicate(arr[k])) { + i = k + 1; + } else { + j = k; + } + } + return i - 1; +} + +/** + * @returns arr.length if predicate is false for all items + */ +function findFirstIdxMonotonous(arr: T[], predicate: (item: T) => boolean): number { + let i = 0; + let j = arr.length; + while (i < j) { + const k = Math.floor((i + j) / 2); + if (predicate(arr[k])) { + j = k; + } else { + i = k + 1; + } + } + return i; } function isWordChar(charCode: number): boolean { diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 409b993d05b..15b63cf0386 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -16,7 +16,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; +import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; import * as model from 'vs/editor/common/model'; import { TokenizationRegistry as TokenizationRegistryImpl } from 'vs/editor/common/tokenizationRegistry'; @@ -2033,3 +2033,33 @@ export interface DocumentOnDropEditProvider { provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; } + +export interface RelatedContextItem { + readonly uri: URI; + readonly range: IRange; +} + +export interface MappedEditsContext { + selections: ISelection[]; + related: RelatedContextItem[]; +} + +export interface MappedEditsProvider { + + /** + * Provider maps code blocks from the chat into a workspace edit. + * + * @param document The document to provide mapped edits for. + * @param codeBlocks Code blocks that come from an LLM's reply. + * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * @param context The context for providing mapped edits. + * @param token A cancellation token. + * @returns A provider result of text edits. + */ + provideMappedEdits( + document: model.ITextModel, + codeBlocks: string[], + context: MappedEditsContext, + token: CancellationToken + ): Promise; +} diff --git a/src/vs/editor/common/services/languageFeatures.ts b/src/vs/editor/common/services/languageFeatures.ts index 2c80887df1a..df53beaecdb 100644 --- a/src/vs/editor/common/services/languageFeatures.ts +++ b/src/vs/editor/common/services/languageFeatures.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LanguageFeatureRegistry, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MappedEditsProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const ILanguageFeaturesService = createDecorator('ILanguageFeaturesService'); @@ -71,6 +71,8 @@ export interface ILanguageFeaturesService { readonly documentOnDropEditProvider: LanguageFeatureRegistry; + readonly mappedEditsProvider: LanguageFeatureRegistry; + // -- setNotebookTypeResolver(resolver: NotebookInfoResolver | undefined): void; diff --git a/src/vs/editor/common/services/languageFeaturesService.ts b/src/vs/editor/common/services/languageFeaturesService.ts index 35d321b4ef2..3ef13891fb7 100644 --- a/src/vs/editor/common/services/languageFeaturesService.ts +++ b/src/vs/editor/common/services/languageFeaturesService.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { LanguageFeatureRegistry, NotebookInfo, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, MappedEditsProvider } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -42,6 +42,7 @@ export class LanguageFeaturesService implements ILanguageFeaturesService { readonly documentSemanticTokensProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentOnDropEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentPasteEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); + readonly mappedEditsProvider: LanguageFeatureRegistry = new LanguageFeatureRegistry(this._score.bind(this)); private _notebookTypeResolver?: NotebookInfoResolver; diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index d92f0c1272e..7cc3add3df1 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -318,7 +318,8 @@ export enum EditorOption { layoutInfo = 142, wrappingInfo = 143, defaultColorDecorators = 144, - colorDecoratorsActivatedOn = 145 + colorDecoratorsActivatedOn = 145, + inlineCompletionsAccessibilityVerbose = 146 } /** diff --git a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts index c16f4d7e9af..289fe8aa96a 100644 --- a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts @@ -12,9 +12,9 @@ import { instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('bracket matching', () => { - let disposables: DisposableStore; let instantiationService: TestInstantiationService; let languageConfigurationService: ILanguageConfigurationService; @@ -31,6 +31,8 @@ suite('bracket matching', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + function createTextModelWithBrackets(text: string) { const languageId = 'bracketMode'; disposables.add(languageService.registerLanguage({ id: languageId })); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts index a57d194fc7f..4817431d189 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts @@ -6,11 +6,14 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; import { registerEditorFeature } from 'vs/editor/common/editorFeatures'; import { DefaultDropProvidersFeature } from 'vs/editor/contrib/dropOrPasteInto/browser/defaultProviders'; +import * as nls from 'vs/nls'; +import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { DropIntoEditorController, changeDropTypeCommandId, dropWidgetVisibleCtx } from './dropIntoEditorController'; - +import { Registry } from 'vs/platform/registry/common/platform'; +import { DropIntoEditorController, changeDropTypeCommandId, defaultProviderConfig, dropWidgetVisibleCtx } from './dropIntoEditorController'; registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController, EditorContributionInstantiation.BeforeFirstInteraction); @@ -32,3 +35,18 @@ registerEditorCommand(new class extends EditorCommand { }); registerEditorFeature(DefaultDropProvidersFeature); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + ...editorConfigurationBaseNode, + properties: { + [defaultProviderConfig]: { + type: 'object', + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, + description: nls.localize('defaultProviderDescription', "Configures the default drop provider to use for content of a given mime type."), + default: {}, + additionalProperties: { + type: 'string', + }, + }, + } +}); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index ca3ed7dfabb..48dae75565c 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -5,7 +5,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { VSDataTransfer, matchesMimeType } from 'vs/base/common/dataTransfer'; import { Disposable } from 'vs/base/common/lifecycle'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -13,7 +13,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { DocumentOnDropEditProvider } from 'vs/editor/common/languages'; +import { DocumentOnDropEdit, DocumentOnDropEditProvider } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; @@ -21,11 +21,14 @@ import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndServ import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState'; import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { sortEditsByYieldTo } from './edit'; import { PostEditWidgetManager } from './postEditWidget'; -import { sortEditsByYieldTo as sortEditsByYieldTo } from './edit'; + +export const defaultProviderConfig = 'editor.experimental.dropIntoEditor.defaultProvider'; export const changeDropTypeCommandId = 'editor.changeDropType'; @@ -49,6 +52,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr constructor( editor: ICodeEditor, @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService private readonly _configService: IConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITreeViewsDnDService private readonly _treeViewsDragAndDropService: ITreeViewsDnDService ) { @@ -108,9 +112,10 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } if (edits.length) { + const activeEditIndex = this.getInitialActiveEditIndex(model, edits); const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop'; // Pass in the parent token here as it tracks cancelling the entire drop operation - await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex: 0, allEdits: edits }, canShowWidget, token); + await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, token); } } finally { tokenSource.dispose(); @@ -124,7 +129,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr this._currentOperation = p; } - private async getDropEdits(providers: DocumentOnDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { + private async getDropEdits(providers: readonly DocumentOnDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { const results = await raceCancellation(Promise.all(providers.map(async provider => { try { const edit = await provider.provideDocumentOnDropEdits(model, position, dataTransfer, tokenSource.token); @@ -136,9 +141,22 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } return undefined; })), tokenSource.token); + const edits = coalesce(results ?? []); - sortEditsByYieldTo(edits); - return edits; + return sortEditsByYieldTo(edits); + } + + private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { + const preferredProviders = this._configService.getValue>(defaultProviderConfig, { resource: model.uri }); + for (const [configMime, desiredId] of Object.entries(preferredProviders)) { + const editIndex = edits.findIndex(edit => + desiredId === edit.providerId + && edit.handledMimeType && matchesMimeType(configMime, [edit.handledMimeType])); + if (editIndex >= 0) { + return editIndex; + } + } + return 0; } private async extractDataTransferData(dragEvent: DragEvent): Promise { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts index f117d88549a..e7b42fe7843 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts @@ -32,21 +32,68 @@ export function sortEditsByYieldTo(edits: T[]): void { +}>(edits: readonly T[]): T[] { function yieldsTo(yTo: DropYieldTo, other: T): boolean { return ('providerId' in yTo && yTo.providerId === other.providerId) || ('mimeType' in yTo && yTo.mimeType === other.handledMimeType); } - edits.sort((a, b) => { - if (a.yieldTo?.some(yTo => yieldsTo(yTo, b))) { - return 1; + // Build list of nodes each node yields to + const yieldsToMap = new Map(); + for (const edit of edits) { + for (const yTo of edit.yieldTo ?? []) { + for (const other of edits) { + if (other === edit) { + continue; + } + + if (yieldsTo(yTo, other)) { + let arr = yieldsToMap.get(edit); + if (!arr) { + arr = []; + yieldsToMap.set(edit, arr); + } + arr.push(other); + } + } + } + } + + if (!yieldsToMap.size) { + return Array.from(edits); + } + + // Topological sort + const visited = new Set(); + const tempStack: T[] = []; + + function visit(nodes: T[]): T[] { + if (!nodes.length) { + return []; } - if (b.yieldTo?.some(yTo => yieldsTo(yTo, a))) { - return -1; + const node = nodes[0]; + if (tempStack.includes(node)) { + console.warn(`Yield to cycle detected for ${node.providerId}`); + return nodes; } - return 0; - }); + if (visited.has(node)) { + return visit(nodes.slice(1)); + } + + let pre: T[] = []; + const yTo = yieldsToMap.get(node); + if (yTo) { + tempStack.push(node); + pre = visit(yTo); + tempStack.pop(); + } + + visited.add(node); + + return [...pre, node, ...visit(nodes.slice(1))]; + } + + return visit(Array.from(edits)); } diff --git a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts new file mode 100644 index 00000000000..44939652de3 --- /dev/null +++ b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DocumentOnDropEdit } from 'vs/editor/common/languages'; +import { sortEditsByYieldTo } from 'vs/editor/contrib/dropOrPasteInto/browser/edit'; + +type DropEdit = DocumentOnDropEdit & { providerId: string | undefined }; + +function createTestEdit(providerId: string, args?: Partial): DropEdit { + return { + label: '', + insertText: '', + providerId, + ...args, + }; +} + +suite('sortEditsByYieldTo', () => { + test('Should noop for empty edits', () => { + const edits: DropEdit[] = []; + + assert.deepStrictEqual(sortEditsByYieldTo(edits), []); + }); + + test('Yielded to edit should get sorted after target', () => { + const edits: DropEdit[] = [ + createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), + createTestEdit('b'), + ]; + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a']); + }); + + test('Should handle chain of yield to', () => { + { + const edits: DropEdit[] = [ + createTestEdit('c', { yieldTo: [{ providerId: 'a' }] }), + createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), + createTestEdit('b'), + ]; + + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a', 'c']); + } + { + const edits: DropEdit[] = [ + createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), + createTestEdit('c', { yieldTo: [{ providerId: 'a' }] }), + createTestEdit('b'), + ]; + + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a', 'c']); + } + }); + + test(`Should not reorder when yield to isn't used`, () => { + const edits: DropEdit[] = [ + createTestEdit('c', { yieldTo: [{ providerId: 'x' }] }), + createTestEdit('a', { yieldTo: [{ providerId: 'y' }] }), + createTestEdit('b'), + ]; + + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['c', 'a', 'b']); + }); +}); diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index ee79563f1f5..6134f4fa401 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -908,13 +908,13 @@ class UnfoldAllRegionsAction extends FoldingAction { } } -class FoldAllRegionsExceptAction extends FoldingAction { +class FoldAllExceptAction extends FoldingAction { constructor() { super({ id: 'editor.foldAllExcept', - label: nls.localize('foldAllExcept.label', "Fold All Regions Except Selected"), - alias: 'Fold All Regions Except Selected', + label: nls.localize('foldAllExcept.label', "Fold All Except Selected"), + alias: 'Fold All Except Selected', precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, @@ -931,13 +931,13 @@ class FoldAllRegionsExceptAction extends FoldingAction { } -class UnfoldAllRegionsExceptAction extends FoldingAction { +class UnfoldAllExceptAction extends FoldingAction { constructor() { super({ id: 'editor.unfoldAllExcept', - label: nls.localize('unfoldAllExcept.label', "Unfold All Regions Except Selected"), - alias: 'Unfold All Regions Except Selected', + label: nls.localize('unfoldAllExcept.label', "Unfold All Except Selected"), + alias: 'Unfold All Except Selected', precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, @@ -1194,8 +1194,8 @@ registerEditorAction(UnfoldAllAction); registerEditorAction(FoldAllBlockCommentsAction); registerEditorAction(FoldAllRegionsAction); registerEditorAction(UnfoldAllRegionsAction); -registerEditorAction(FoldAllRegionsExceptAction); -registerEditorAction(UnfoldAllRegionsExceptAction); +registerEditorAction(FoldAllExceptAction); +registerEditorAction(UnfoldAllExceptAction); registerEditorAction(ToggleFoldAction); registerEditorAction(GotoParentFoldAction); registerEditorAction(GotoPreviousFoldAction); diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts index 691545a3e9e..c06e6bac4a7 100644 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ b/src/vs/editor/contrib/hover/browser/contentHover.ts @@ -854,7 +854,6 @@ export class ContentHoverWidget extends ResizableContentWidget { width = dom.getTotalWidth(containerDomNode); this._contentWidth = width; this._updateMinimumWidth(); - this._updateResizableNodeMaxDimensions(); this._resizableNode.layout(height, width); if (this._hasHorizontalScrollbar()) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts index 61787b29ac8..51b2d4fc562 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts @@ -22,11 +22,13 @@ import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; +import { localize } from 'vs/nls'; import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class InlineCompletionsController extends Disposable { static ID = 'editor.contrib.inlineCompletionsController'; @@ -73,11 +75,11 @@ export class InlineCompletionsController extends Disposable { @ILanguageFeatureDebounceService private readonly debounceService: ILanguageFeatureDebounceService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IAudioCueService private readonly audioCueService: IAudioCueService, + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); this._register(new InlineCompletionContextKeys(this.contextKeyService, this.model)); - this._register(Event.runAndSubscribe(editor.onDidChangeModel, () => transaction(tx => { /** @description onDidChangeModel */ this.model.set(undefined, tx); @@ -147,7 +149,7 @@ export class InlineCompletionsController extends Disposable { this._register(this.editor.onDidBlurEditorWidget(() => { // This is a hidden setting very useful for debugging - if (this.configurationService.getValue('editor.inlineSuggest.keepOnBlur') || + if (this.contextKeyService.getContextKeyValue('accessibleViewIsShown') || this.configurationService.getValue('editor.inlineSuggest.keepOnBlur') || editor.getOption(EditorOption.inlineSuggest).keepOnBlur) { return; } @@ -190,13 +192,29 @@ export class InlineCompletionsController extends Disposable { const lineText = model.textModel.getLineContent(state.ghostText.lineNumber); this.audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => { if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - alert(state.ghostText.renderForScreenReader(lineText)); + this.provideScreenReaderUpdate(state.ghostText.renderForScreenReader(lineText)); } }); } })); this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this.instantiationService)); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('accessibility.verbosity.inlineCompletions')) { + this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this.configurationService.getValue('accessibility.verbosity.inlineCompletions') }); + } + })); + this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this.configurationService.getValue('accessibility.verbosity.inlineCompletions') }); + } + + private provideScreenReaderUpdate(content: string): void { + const accessibleViewShowing = this.contextKeyService.getContextKeyValue('accessibleViewIsShown'); + const accessibleViewKeybinding = this._keybindingService.lookupKeybinding('editor.action.accessibleView'); + let hint: string | undefined; + if (!accessibleViewShowing && accessibleViewKeybinding && this.editor.getOption(EditorOption.inlineCompletionsAccessibilityVerbose)) { + hint = localize('showAccessibleViewHint', "Inspect this in the accessible view ({0})", accessibleViewKeybinding.getAriaLabel()); + } + hint ? alert(content + ', ' + hint) : alert(content); } /** diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 0e3355907af..11a5c6c4f27 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -953,8 +953,8 @@ export class TransposeAction extends EditorAction { constructor() { super({ id: 'editor.action.transpose', - label: nls.localize('editor.transpose', "Transpose characters around the cursor"), - alias: 'Transpose characters around the cursor', + label: nls.localize('editor.transpose', "Transpose Characters around the Cursor"), + alias: 'Transpose Characters around the Cursor', precondition: EditorContextKeys.writable }); } diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 6193ce53e26..8db880a15ad 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -117,7 +117,7 @@ export class MoveLinesCommand implements ICommand { const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber)); const newSpaceCnt = movingLineMatchResult + indentUtils.getSpaceCnt(oldIndentation, tabSize); const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces); - insertingText = newIndentation + this.trimLeft(movingLineText); + insertingText = newIndentation + this.trimStart(movingLineText); } else { // no enter rule matches, let's check indentatin rules then. virtualModel.getLineContent = (lineNumber: number) => { @@ -141,7 +141,7 @@ export class MoveLinesCommand implements ICommand { const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize); if (newSpaceCnt !== oldSpaceCnt) { const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces); - insertingText = newIndentation + this.trimLeft(movingLineText); + insertingText = newIndentation + this.trimStart(movingLineText); } } } @@ -272,7 +272,7 @@ export class MoveLinesCommand implements ICommand { enterPrefix = indentConverter.unshiftIndent(enter.indentation) + enter.appendText; } const movingLineText = model.getLineContent(line); - if (this.trimLeft(movingLineText).indexOf(this.trimLeft(enterPrefix)) >= 0) { + if (this.trimStart(movingLineText).indexOf(this.trimStart(enterPrefix)) >= 0) { const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(line)); let newIndentation = strings.getLeadingWhitespace(enterPrefix); const indentMetadataOfMovelingLine = getIndentMetadata(model, line, this._languageConfigurationService); @@ -354,7 +354,7 @@ export class MoveLinesCommand implements ICommand { return this.parseEnterResult(model, indentConverter, tabSize, line, enter); } - private trimLeft(str: string) { + private trimStart(str: string) { return str.replace(/^\s+/, ''); } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts index 3d5b4942690..ece8daa4e51 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts @@ -33,7 +33,7 @@ export class ToggleStickyScroll extends Action2 { }, menu: [ { id: MenuId.CommandPalette }, - { id: MenuId.MenubarViewMenu, group: '5_editor', order: 2 }, + { id: MenuId.MenubarAppearanceMenu, group: '4_editor', order: 3 }, { id: MenuId.StickyScrollContext } ] }); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index 265e0c458ff..37647eccdf6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IActiveCodeEditor, ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IActiveCodeEditor, ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { StickyScrollWidget, StickyScrollWidgetState } from './stickyScrollWidget'; @@ -26,7 +26,7 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import * as dom from 'vs/base/browser/dom'; import { StickyRange } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollElement'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export interface IStickyScrollController { get stickyScrollCandidateProvider(): IStickyLineCandidateProvider; @@ -63,6 +63,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib private _focused = false; private _positionRevealed = false; private _onMouseDown = false; + private _endLineNumbers: number[] = []; + private _showEndForLine: number | null = null; constructor( private readonly _editor: ICodeEditor, @@ -79,7 +81,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._register(this._stickyScrollWidget); this._register(this._stickyLineCandidateProvider); - this._widgetState = new StickyScrollWidgetState([], 0); + this._widgetState = new StickyScrollWidgetState([], [], 0); this._readConfiguration(); const stickyScrollDomNode = this._stickyScrollWidget.getDomNode(); this._register(this._editor.onDidChangeConfiguration(e => { @@ -183,8 +185,19 @@ export class StickyScrollController extends Disposable implements IEditorContrib } private _revealPosition(position: IPosition): void { + this._reveaInEditor(position, () => this._editor.revealPosition(position)); + } + + private _revealLineInCenterIfOutsideViewport(position: IPosition): void { + this._reveaInEditor(position, () => this._editor.revealLineInCenterIfOutsideViewport(position.lineNumber, ScrollType.Smooth)); + } + + private _reveaInEditor(position: IPosition, revealFunction: () => void): void { + if (this._focused) { + this._disposeFocusStickyScrollStore(); + } this._positionRevealed = true; - this._editor.revealPosition(position); + revealFunction(); this._editor.setSelection(Range.fromPositions(position)); this._editor.focus(); } @@ -223,35 +236,59 @@ export class StickyScrollController extends Disposable implements IEditorContrib }; }; - this._register(this._editor.onMouseUp((mouseEvent: IEditorMouseEvent) => { - if (mouseEvent.target.type !== MouseTargetType.OVERLAY_WIDGET || mouseEvent.target.detail !== this._stickyScrollWidget.getId()) { - // not hovering over our widget - return; - } - if (mouseEvent.event.ctrlKey || mouseEvent.event.shiftKey || mouseEvent.event.altKey || mouseEvent.event.metaKey) { + const stickyScrollWidgetDomNode = this._stickyScrollWidget.getDomNode(); + this._register(dom.addStandardDisposableListener(stickyScrollWidgetDomNode, dom.EventType.CLICK, (mouseEvent: IMouseEvent) => { + if (mouseEvent.ctrlKey || mouseEvent.altKey || mouseEvent.metaKey) { // modifier pressed return; } - if (!mouseEvent.event.leftButton) { + if (!mouseEvent.leftButton) { // not left click return; } - - let position = this._stickyScrollWidget.getEditorPositionFromNode(mouseEvent.target.element); + if (mouseEvent.shiftKey) { + // shift click + const lineIndex = this._stickyScrollWidget.getStickyLineIndexFromChildDomNode(mouseEvent.target); + if (lineIndex === null) { + return; + } + const position = new Position(this._endLineNumbers[lineIndex], 1); + this._revealLineInCenterIfOutsideViewport(position); + return; + } + // normal click + let position = this._stickyScrollWidget.getEditorPositionFromNode(mouseEvent.target); if (!position) { - const lineNumber = this._stickyScrollWidget.getLineNumberFromChildDomNode(mouseEvent.target.element); + const lineNumber = this._stickyScrollWidget.getLineNumberFromChildDomNode(mouseEvent.target); if (lineNumber === null) { // not hovering a sticky scroll line return; } position = new Position(lineNumber, 1); } - - if (this._focused) { - this._disposeFocusStickyScrollStore(); - } this._revealPosition(position); })); + this._register(dom.addStandardDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_MOVE, (mouseEvent: IMouseEvent) => { + if (mouseEvent.shiftKey) { + const currentEndForLineIndex = this._stickyScrollWidget.getStickyLineIndexFromChildDomNode(mouseEvent.target); + if (currentEndForLineIndex === null || this._showEndForLine !== null && this._showEndForLine === currentEndForLineIndex) { + return; + } + this._showEndForLine = currentEndForLineIndex; + this._renderStickyScroll(); + return; + } + if (this._showEndForLine !== null) { + this._showEndForLine = null; + this._renderStickyScroll(); + } + })); + this._register(dom.addDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_LEAVE, (e) => { + if (this._showEndForLine !== null) { + this._showEndForLine = null; + this._renderStickyScroll(); + } + })); this._register(gesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, _keyboardEvent]) => { const mouseTarget = getMouseEventTarget(mouseEvent); @@ -344,18 +381,25 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._editor.addOverlayWidget(this._stickyScrollWidget); this._sessionStore.add(this._editor.onDidScrollChange((e) => { if (e.scrollTopChanged) { + this._showEndForLine = null; this._renderStickyScroll(); } })); this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize())); this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e))); - this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => this._renderStickyScroll())); + this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => { + this._showEndForLine = null; + this._renderStickyScroll(); + })); this._enabled = true; } const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers); if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { - this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => this._renderStickyScroll())); + this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => { + this._showEndForLine = null; + this._renderStickyScroll(); + })); } } @@ -392,7 +436,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib const stickyLineVersion = this._stickyLineCandidateProvider.getVersionId(); if (stickyLineVersion === undefined || stickyLineVersion === model.getVersionId()) { this._widgetState = this.findScrollWidgetState(); - this._stickyScrollVisibleContextKey.set(!(this._widgetState.lineNumbers.length === 0)); + this._stickyScrollVisibleContextKey.set(!(this._widgetState.startLineNumbers.length === 0)); if (!this._focused) { this._stickyScrollWidget.setState(this._widgetState); @@ -430,7 +474,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib const maxNumberStickyLines = Math.min(this._maxStickyLines, this._editor.getOption(EditorOption.stickyScroll).maxLineCount); const scrollTop: number = this._editor.getScrollTop(); let lastLineRelativePosition: number = 0; - const lineNumbers: number[] = []; + const startLineNumbers: number[] = []; + const endLineNumbers: number[] = []; const arrayVisibleRanges = this._editor.getVisibleRanges(); if (arrayVisibleRanges.length !== 0) { const fullVisibleRange = new StickyRange(arrayVisibleRanges[0].startLineNumber, arrayVisibleRanges[arrayVisibleRanges.length - 1].endLineNumber); @@ -448,20 +493,23 @@ export class StickyScrollController extends Disposable implements IEditorContrib const bottomOfEndLine = this._editor.getBottomForLineNumber(end) - scrollTop; if (topOfElementAtDepth > topOfEndLine && topOfElementAtDepth <= bottomOfEndLine) { - lineNumbers.push(start); + startLineNumbers.push(start); + endLineNumbers.push(end + 1); lastLineRelativePosition = bottomOfEndLine - bottomOfElementAtDepth; break; } else if (bottomOfElementAtDepth > bottomOfBeginningLine && bottomOfElementAtDepth <= bottomOfEndLine) { - lineNumbers.push(start); + startLineNumbers.push(start); + endLineNumbers.push(end + 1); } - if (lineNumbers.length === maxNumberStickyLines) { + if (startLineNumbers.length === maxNumberStickyLines) { break; } } } } - return new StickyScrollWidgetState(lineNumbers, lastLineRelativePosition); + this._endLineNumbers = endLineNumbers; + return new StickyScrollWidgetState(startLineNumbers, endLineNumbers, lastLineRelativePosition, this._showEndForLine); } override dispose(): void { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts index 13051e80058..f1f50f9ced7 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; @@ -45,18 +45,20 @@ export interface IStickyModelProvider { update(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise; } -export class StickyModelProvider implements IStickyModelProvider { +export class StickyModelProvider extends Disposable implements IStickyModelProvider { private _modelProviders: IStickyModelCandidateProvider[] = []; private _modelPromise: CancelablePromise | null = null; - private _updateScheduler: Delayer = new Delayer(300); - private readonly _store: DisposableStore; + private _updateScheduler: Delayer = this._register(new Delayer(300)); + private readonly _updateOperation: DisposableStore = this._register(new DisposableStore()); constructor( private readonly _editor: ICodeEditor, @ILanguageConfigurationService readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService readonly _languageFeaturesService: ILanguageFeaturesService, - defaultModel: string) { + defaultModel: string + ) { + super(); const stickyModelFromCandidateOutlineProvider = new StickyModelFromCandidateOutlineProvider(_languageFeaturesService); const stickyModelFromSyntaxFoldingProvider = new StickyModelFromCandidateSyntaxFoldingProvider(this._editor, _languageFeaturesService); @@ -76,8 +78,6 @@ export class StickyModelProvider implements IStickyModelProvider { this._modelProviders.push(stickyModelFromIndentationFoldingProvider); break; } - - this._store = new DisposableStore(); } private _cancelModelPromise(): void { @@ -89,11 +89,11 @@ export class StickyModelProvider implements IStickyModelProvider { public async update(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise { - this._store.clear(); - this._store.add({ + this._updateOperation.clear(); + this._updateOperation.add({ dispose: () => { this._cancelModelPromise(); - this._updateScheduler?.cancel(); + this._updateScheduler.cancel(); } }); this._cancelModelPromise(); @@ -113,7 +113,7 @@ export class StickyModelProvider implements IStickyModelProvider { } switch (status) { case Status.CANCELED: - this._store.clear(); + this._updateOperation.clear(); return null; case Status.VALID: return modelProvider.stickyModel; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 391ddd6c978..3269b037cd6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts @@ -11,7 +11,6 @@ import { EditorOption, IEditorStickyScrollOptions } from 'vs/editor/common/confi import { RunOnceScheduler } from 'vs/base/common/async'; import { Range } from 'vs/editor/common/core/range'; import { binarySearch } from 'vs/base/common/arrays'; -import { isEqual } from 'vs/base/common/resources'; import { Event, Emitter } from 'vs/base/common/event'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { StickyModelProvider, IStickyModelProvider } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider'; @@ -39,7 +38,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi static readonly ID = 'store.contrib.stickyScrollController'; - private readonly _onDidChangeStickyScroll = this._store.add(new Emitter()); + private readonly _onDidChangeStickyScroll = this._register(new Emitter()); public readonly onDidChangeStickyScroll = this._onDidChangeStickyScroll.event; private readonly _editor: ICodeEditor; @@ -58,7 +57,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi ) { super(); this._editor = editor; - this._sessionStore = new DisposableStore(); + this._sessionStore = this._register(new DisposableStore()); this._updateSoon = this._register(new RunOnceScheduler(() => this.update(), 50)); this._register(this._editor.onDidChangeConfiguration(e => { @@ -69,27 +68,31 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi this.readConfiguration(); } - override dispose(): void { - super.dispose(); - this._sessionStore.dispose(); - } - private readConfiguration() { + this._stickyModelProvider = null; + this._sessionStore.clear(); + this._options = this._editor.getOption(EditorOption.stickyScroll); if (!this._options.enabled) { - this._sessionStore.clear(); return; } - this._stickyModelProvider = new StickyModelProvider( + this._stickyModelProvider = this._sessionStore.add(new StickyModelProvider( this._editor, this._languageConfigurationService, this._languageFeaturesService, this._options.defaultModel - ); + )); - this._sessionStore.add(this._editor.onDidChangeModel(() => this.update())); + this._sessionStore.add(this._editor.onDidChangeModel(() => { + // We should not show an old model for a different file, it will always be wrong. + // So we clear the model here immediately and then trigger an update. + this._model = null; + this._onDidChangeStickyScroll.fire(); + + this.update(); + })); this._sessionStore.add(this._editor.onDidChangeHiddenAreas(() => this.update())); this._sessionStore.add(this._editor.onDidChangeModelContent(() => this._updateSoon.schedule())); this._sessionStore.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => this.update())); @@ -110,24 +113,20 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi private async updateStickyModel(token: CancellationToken): Promise { if (!this._editor.hasModel() || !this._stickyModelProvider) { + this._model = null; return; } const textModel = this._editor.getModel(); const modelVersionId = textModel.getVersionId(); - const isDifferentModel = this._model ? !isEqual(this._model.uri, textModel.uri) : false; - // Clear sticky scroll to not show stale data for too long - const resetHandle = isDifferentModel ? setTimeout(() => { - if (!token.isCancellationRequested) { - this._model = new StickyModel(textModel.uri, textModel.getVersionId(), undefined, undefined); - this._onDidChangeStickyScroll.fire(); - } - }, 75) : undefined; + const model = await this._stickyModelProvider.update(textModel, modelVersionId, token); + if (token.isCancellationRequested) { + // the computation was canceled, so do not overwrite the model + return; + } - this._model = await this._stickyModelProvider.update(textModel, modelVersionId, token); - - clearTimeout(resetHandle); + this._model = model; } private updateIndex(index: number) { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 61914f776c7..79e9f479add 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -18,8 +18,10 @@ import { CharacterMapping, RenderLineInput, renderViewLine } from 'vs/editor/com export class StickyScrollWidgetState { constructor( - readonly lineNumbers: number[], - readonly lastLineRelativePosition: number + readonly startLineNumbers: number[], + readonly endLineNumbers: number[], + readonly lastLineRelativePosition: number, + readonly showEndForLine: number | null = null ) { } } @@ -102,11 +104,15 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { dom.clearNode(this._linesDomNode); this._stickyLines = []; const editorLineHeight = this._editor.getOption(EditorOption.lineHeight); - const futureWidgetHeight = state.lineNumbers.length * editorLineHeight + state.lastLineRelativePosition; + const futureWidgetHeight = state.startLineNumbers.length * editorLineHeight + state.lastLineRelativePosition; if (futureWidgetHeight > 0) { this._lastLineRelativePosition = state.lastLineRelativePosition; - this._lineNumbers = state.lineNumbers; + const lineNumbers = [...state.startLineNumbers]; + if (state.showEndForLine !== null) { + lineNumbers[state.showEndForLine] = state.endLineNumbers[state.showEndForLine]; + } + this._lineNumbers = lineNumbers; } else { this._lastLineRelativePosition = 0; this._lineNumbers = []; @@ -299,7 +305,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } private _getRenderedStickyLineFromChildDomNode(domNode: HTMLElement | null): RenderedStickyLine | null { - const index = this._getStickyLineIndexFromChildDomNode(domNode); + const index = this.getStickyLineIndexFromChildDomNode(domNode); if (index === null || index < 0 || index >= this._stickyLines.length) { return null; } @@ -310,7 +316,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { * Given a child dom node, tries to find the line number attribute * that was stored in the node. Returns null if none is found. */ - private _getStickyLineIndexFromChildDomNode(domNode: HTMLElement | null): number | null { + getStickyLineIndexFromChildDomNode(domNode: HTMLElement | null): number | null { while (domNode && domNode !== this._rootDomNode) { const line = domNode.getAttribute(STICKY_LINE_INDEX_ATTR); if (line) { diff --git a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts index 909bddf9801..906d1299bb8 100644 --- a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts +++ b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts @@ -161,27 +161,27 @@ suite('Sticky Scroll Tests', () => { editor.setScrollTop(1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [1]); + assert.deepStrictEqual(state.startLineNumbers, [1]); editor.setScrollTop(lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [1]); + assert.deepStrictEqual(state.startLineNumbers, [1]); editor.setScrollTop(4 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, []); + assert.deepStrictEqual(state.startLineNumbers, []); editor.setScrollTop(8 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [7, 9]); + assert.deepStrictEqual(state.startLineNumbers, [7, 9]); editor.setScrollTop(9 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [7, 9]); + assert.deepStrictEqual(state.startLineNumbers, [7, 9]); editor.setScrollTop(10 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [7]); + assert.deepStrictEqual(state.startLineNumbers, [7]); stickyScrollController.dispose(); stickyScrollController.stickyScrollCandidateProvider.dispose(); @@ -212,23 +212,23 @@ suite('Sticky Scroll Tests', () => { editor.setScrollTop(1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [1]); + assert.deepStrictEqual(state.startLineNumbers, [1]); editor.setScrollTop(lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, []); + assert.deepStrictEqual(state.startLineNumbers, []); editor.setScrollTop(6 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [7, 9]); + assert.deepStrictEqual(state.startLineNumbers, [7, 9]); editor.setScrollTop(7 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [7]); + assert.deepStrictEqual(state.startLineNumbers, [7]); editor.setScrollTop(10 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, []); + assert.deepStrictEqual(state.startLineNumbers, []); stickyScrollController.dispose(); stickyScrollController.stickyScrollCandidateProvider.dispose(); @@ -306,23 +306,23 @@ suite('Sticky Scroll Tests', () => { editor.setScrollTop(1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [1, 2]); + assert.deepStrictEqual(state.startLineNumbers, [1, 2]); editor.setScrollTop(lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [1, 2]); + assert.deepStrictEqual(state.startLineNumbers, [1, 2]); editor.setScrollTop(2 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [1]); + assert.deepStrictEqual(state.startLineNumbers, [1]); editor.setScrollTop(3 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, [1]); + assert.deepStrictEqual(state.startLineNumbers, [1]); editor.setScrollTop(4 * lineHeight + 1); state = stickyScrollController.findScrollWidgetState(); - assert.deepStrictEqual(state.lineNumbers, []); + assert.deepStrictEqual(state.startLineNumbers, []); stickyScrollController.dispose(); stickyScrollController.stickyScrollCandidateProvider.dispose(); diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 887fb23cb2d..6450e989bce 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -143,7 +143,7 @@ export class SuggestController implements IEditorContribution { // context key: update insert/replace mode const ctxInsertMode = SuggestContext.InsertMode.bindTo(_contextKeyService); ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode); - this.model.onDidTrigger(() => ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode)); + this._toDispose.add(this.model.onDidTrigger(() => ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode))); this.widget = this._toDispose.add(new IdleValue(() => { diff --git a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts index 1ff62f2cb35..d4df28ea332 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts @@ -12,10 +12,10 @@ import { CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/lan import { CompletionOptions, provideSuggestionItems, SnippetSortOrder } from 'vs/editor/contrib/suggest/browser/suggest'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Suggest', function () { - let model: TextModel; let registration: IDisposable; let registry: LanguageFeatureRegistry; @@ -54,37 +54,43 @@ suite('Suggest', function () { model.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('sort - snippet inline', async function () { - const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Inline)); + const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Inline)); assert.strictEqual(items.length, 3); assert.strictEqual(items[0].completion.label, 'aaa'); assert.strictEqual(items[1].completion.label, 'fff'); assert.strictEqual(items[2].completion.label, 'zzz'); + disposable.dispose(); }); test('sort - snippet top', async function () { - const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Top)); + const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Top)); assert.strictEqual(items.length, 3); assert.strictEqual(items[0].completion.label, 'aaa'); assert.strictEqual(items[1].completion.label, 'zzz'); assert.strictEqual(items[2].completion.label, 'fff'); + disposable.dispose(); }); test('sort - snippet bottom', async function () { - const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Bottom)); + const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Bottom)); assert.strictEqual(items.length, 3); assert.strictEqual(items[0].completion.label, 'fff'); assert.strictEqual(items[1].completion.label, 'aaa'); assert.strictEqual(items[2].completion.label, 'zzz'); + disposable.dispose(); }); test('sort - snippet none', async function () { - const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, new Set().add(CompletionItemKind.Snippet))); + const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, new Set().add(CompletionItemKind.Snippet))); assert.strictEqual(items.length, 1); assert.strictEqual(items[0].completion.label, 'fff'); + disposable.dispose(); }); - test('only from', function () { + test('only from', function (callback) { const foo: any = { triggerCharacters: [], @@ -102,11 +108,13 @@ suite('Suggest', function () { }; const registration = registry.register({ pattern: 'bar/path', scheme: 'foo' }, foo); - provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, undefined, new Set().add(foo))).then(({ items }) => { + provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, undefined, new Set().add(foo))).then(({ items, disposable }) => { registration.dispose(); assert.strictEqual(items.length, 1); assert.ok(items[0].provider === foo); + disposable.dispose(); + callback(); }); }); @@ -142,7 +150,7 @@ suite('Suggest', function () { }; const registration = registry.register({ pattern: 'bar/path', scheme: 'foo' }, foo); - const { items } = await provideSuggestionItems(registry, model, new Position(0, 0), new CompletionOptions(undefined, undefined, new Set().add(foo))); + const { items, disposable } = await provideSuggestionItems(registry, model, new Position(0, 0), new CompletionOptions(undefined, undefined, new Set().add(foo))); registration.dispose(); assert.strictEqual(items.length, 2); @@ -152,5 +160,6 @@ suite('Suggest', function () { assert.strictEqual(a.isInvalid, false); assert.strictEqual(b.completion.label, 'two'); assert.strictEqual(b.isInvalid, true); + disposable.dispose(); }); }); diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 4aa2558480f..8c35ee5b269 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -8,13 +8,13 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPosit import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IQuickInputService, IQuickInputButton, IQuickPickItem, IQuickPick, IInputBox, IQuickNavigateConfiguration, IPickOptions, QuickPickInput, IInputOptions } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickInputButton, IQuickPickItem, IQuickPick, IInputBox, IQuickNavigateConfiguration, IPickOptions, QuickPickInput, IInputOptions, IQuickWidget } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorScopedLayoutService } from 'vs/editor/standalone/browser/standaloneLayoutService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IQuickInputControllerHost, QuickInputController } from 'vs/platform/quickinput/browser/quickInput'; +import { QuickInputController, IQuickInputControllerHost } from 'vs/platform/quickinput/browser/quickInputController'; import { QuickInputService } from 'vs/platform/quickinput/browser/quickInputService'; import { once } from 'vs/base/common/functional'; import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; @@ -111,6 +111,10 @@ export class StandaloneQuickInputService implements IQuickInputService { return this.activeService.createInputBox(); } + createQuickWidget(): IQuickWidget { + return this.activeService.createQuickWidget(); + } + focus(): void { return this.activeService.focus(); } diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index acca9e7cde3..28a17729768 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -45,7 +45,8 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { BrandedService, IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -176,6 +177,7 @@ export function createCodeEditorServices(disposables: DisposableStore, services: }; define(IAccessibilityService, TestAccessibilityService); + define(IKeybindingService, MockKeybindingService); define(IClipboardService, TestClipboardService); define(IEditorWorkerService, TestEditorWorkerService); defineInstance(IOpenerService, NullOpenerService); diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts index abf6200a8fa..2a21907778d 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -5,12 +5,15 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets'; import { SmallImmutableSet, DenseKeyProvider } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet'; import { Token, TokenKind } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('Bracket Pair Colorizer - Brackets', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('Basic', () => { const languageId = 'testMode1'; const denseKeyProvider = new DenseKeyProvider(); @@ -24,7 +27,7 @@ suite('Bracket Pair Colorizer - Brackets', () => { }; const disposableStore = new DisposableStore(); - const languageConfigService = new TestLanguageConfigurationService(); + const languageConfigService = disposableStore.add(new TestLanguageConfigurationService()); disposableStore.add(languageConfigService.register(languageId, { brackets: [ ['{', '}'], ['[', ']'], ['(', ')'], diff --git a/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json index 2b8f1b4ab1a..5cbd3c45666 100644 --- a/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[29,37)", "innerChanges": [ { - "originalRange": "[29,1 -> 60,48]", - "modifiedRange": "[29,1 -> 36,66]" + "originalRange": "[29,1 -> 61,1]", + "modifiedRange": "[29,1 -> 37,1]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json index 8ac0682b508..b1de25a5907 100644 --- a/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[6,11)", "innerChanges": [ { - "originalRange": "[6,2 -> 11,49]", - "modifiedRange": "[6,2 -> 6,91]" + "originalRange": "[6,1 -> 11,49]", + "modifiedRange": "[6,1 -> 6,91]" }, { "originalRange": "[12,76 -> 12,76]", diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json index 5a679831447..20190a83798 100644 --- a/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json @@ -21,8 +21,8 @@ "modifiedRange": "[53,6 -> 53,45]" }, { - "originalRange": "[52,77 -> 55,53]", - "modifiedRange": "[53,98 -> 65,66]" + "originalRange": "[52,77 -> 56,1]", + "modifiedRange": "[53,98 -> 66,1]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json index 92f5e20707f..0b00ea98beb 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json @@ -47,8 +47,8 @@ "modifiedRange": "[8,12)", "innerChanges": [ { - "originalRange": "[11,10 -> 20,25]", - "modifiedRange": "[8,10 -> 11,51]" + "originalRange": "[11,10 -> 21,1]", + "modifiedRange": "[8,10 -> 12,1]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json index f6817defb49..3835a5ceb3c 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json @@ -43,8 +43,8 @@ "modifiedRange": "[14,18)", "innerChanges": [ { - "originalRange": "[14,120 -> 14,211]", - "modifiedRange": "[14,120 -> 17,1]" + "originalRange": "[14,120 -> 15,1]", + "modifiedRange": "[14,120 -> 18,1]" } ] }, @@ -65,8 +65,8 @@ "modifiedRange": "[21,18 -> 21,43]" }, { - "originalRange": "[17,71 -> 23,4]", - "modifiedRange": "[21,74 -> 21,81]" + "originalRange": "[17,71 -> 24,1]", + "modifiedRange": "[21,74 -> 22,1]" } ] }, @@ -93,8 +93,8 @@ "modifiedRange": "[26,38)", "innerChanges": [ { - "originalRange": "[28,1 -> 30,31]", - "modifiedRange": "[26,1 -> 37,9]" + "originalRange": "[28,1 -> 31,1]", + "modifiedRange": "[26,1 -> 38,1]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json index d8eac8523e6..c7cd88a788b 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json @@ -27,8 +27,8 @@ "modifiedRange": "[17,80)", "innerChanges": [ { - "originalRange": "[13,10 -> 14,43]", - "modifiedRange": "[17,10 -> 79,2]" + "originalRange": "[13,10 -> 15,1]", + "modifiedRange": "[17,10 -> 80,1]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/advanced.expected.diff.json index aa65f823a85..7a52e0e84aa 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-too-much-minimization/advanced.expected.diff.json @@ -13,8 +13,8 @@ "modifiedRange": "[9,15)", "innerChanges": [ { - "originalRange": "[9,124 -> 9,124]", - "modifiedRange": "[9,124 -> 14,143]" + "originalRange": "[9,124 -> 10,1]", + "modifiedRange": "[9,124 -> 15,1]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json index 85a9a2fe928..66e738ab041 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json @@ -23,8 +23,8 @@ "modifiedRange": "[7,18)", "innerChanges": [ { - "originalRange": "[7,5 -> 12,17]", - "modifiedRange": "[7,5 -> 16,7]" + "originalRange": "[7,1 -> 13,1]", + "modifiedRange": "[7,1 -> 17,1]" }, { "originalRange": "[13,6 -> 13,11]", diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8914c4f0036..544197938c2 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2460,6 +2460,7 @@ declare namespace monaco.editor { * Moves this line range by the given offset of line numbers. */ delta(offset: number): LineRange; + deltaLength(offset: number): LineRange; /** * The number of lines this line range spans. */ @@ -2545,6 +2546,7 @@ declare namespace monaco.editor { constructor(original: LineRange, modified: LineRange); toString(): string; flip(): SimpleLineRangeMapping; + join(other: SimpleLineRangeMapping): SimpleLineRangeMapping; } export interface IDimension { width: number; @@ -3888,6 +3890,10 @@ declare namespace monaco.editor { * Controls whether the editor receives tabs or defers them to the workbench for navigation. */ tabFocusMode?: boolean; + /** + * Controls whether the accessibility hint should be provided to screen reader users when an inline completion is shown. + */ + inlineCompletionsAccessibilityVerbose?: boolean; } export interface IDiffEditorBaseOptions { @@ -5024,7 +5030,8 @@ declare namespace monaco.editor { layoutInfo = 142, wrappingInfo = 143, defaultColorDecorators = 144, - colorDecoratorsActivatedOn = 145 + colorDecoratorsActivatedOn = 145, + inlineCompletionsAccessibilityVerbose = 146 } export const EditorOptions: { @@ -5148,6 +5155,7 @@ declare namespace monaco.editor { stopRenderingLineAfter: IEditorOption; suggest: IEditorOption>>; inlineSuggest: IEditorOption>>; + inlineCompletionsAccessibilityVerbose: IEditorOption; suggestFontSize: IEditorOption; suggestLineHeight: IEditorOption; suggestOnTriggerCharacters: IEditorOption; @@ -7885,6 +7893,30 @@ declare namespace monaco.languages { provideDocumentRangeSemanticTokens(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; } + export interface RelatedContextItem { + readonly uri: Uri; + readonly range: IRange; + } + + export interface MappedEditsContext { + selections: ISelection[]; + related: RelatedContextItem[]; + } + + export interface MappedEditsProvider { + /** + * Provider maps code blocks from the chat into a workspace edit. + * + * @param document The document to provide mapped edits for. + * @param codeBlocks Code blocks that come from an LLM's reply. + * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * @param context The context for providing mapped edits. + * @param token A cancellation token. + * @returns A provider result of text edits. + */ + provideMappedEdits(document: editor.ITextModel, codeBlocks: string[], context: MappedEditsContext, token: CancellationToken): Promise; + } + export interface ILanguageExtensionPoint { id: string; extensions?: string[]; diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 1cea5ac88c0..e4422c393d6 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -6,7 +6,6 @@ import * as dom from 'vs/base/browser/dom'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { matchesFuzzy2 } from 'vs/base/common/filters'; import { Codicon } from 'vs/base/common/codicons'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -44,11 +43,6 @@ interface IActionMenuTemplateData { readonly keybinding: KeybindingLabel; } -interface IActionFilteredItems { - readonly label: string | undefined; - readonly index: number; -} - export const enum ActionListItemKind { Action = 'action', Header = 'header' @@ -154,6 +148,15 @@ class AcceptSelectedEvent extends UIEvent { class PreviewSelectedEvent extends UIEvent { constructor() { super('previewSelectedAction'); } } + +function getKeyboardNavigationLabel(item: IActionListItem): string | undefined { + // Filter out header vs. action + if (item.kind === 'action') { + return item.label; + } + return undefined; +} + export class ActionList extends Disposable { public readonly domNode: HTMLElement; @@ -164,10 +167,6 @@ export class ActionList extends Disposable { private readonly _headerLineHeight = 26; private readonly _allMenuItems: readonly IActionListItem[]; - private readonly _allMenuItemsFiltered: IActionFilteredItems[] = []; - - private keypresses: string[] = []; - private timeout: number | null = null; constructor( user: string, @@ -191,6 +190,8 @@ export class ActionList extends Disposable { new HeaderRenderer(), ], { keyboardSupport: false, + typeNavigationEnabled: true, + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel }, accessibilityProvider: { getAriaLabel: element => { if (element.kind === ActionListItemKind.Action) { @@ -207,18 +208,15 @@ export class ActionList extends Disposable { getWidgetRole: () => 'listbox', }, })); + this._list.style(defaultListStyles); this._register(this._list.onMouseClick(e => this.onListClick(e))); this._register(this._list.onMouseOver(e => this.onListHover(e))); this._register(this._list.onDidChangeFocus(() => this._list.domFocus())); this._register(this._list.onDidChangeSelection(e => this.onListSelection(e))); - this._register(this._list.onKeyPress(e => this.onKeyPress(e))); this._allMenuItems = items; - const menuItems = this._allMenuItems; - this._allMenuItemsFiltered = menuItems.flatMap((obj, index) => (obj.kind === `action` && !obj.disabled) ? [{ label: obj.label, index }] : []); - this._list.splice(0, this._list.length, this._allMenuItems); if (this._list.length) { @@ -313,40 +311,6 @@ export class ActionList extends Disposable { this._list.setFocus([]); } } - - private onKeyPress(e: KeyboardEvent) { - this.keypresses.push(e.key); - this.listFuzzyMatch(this.keypresses.join('')); - - // Clear the previous timeout (if any) - if (this.timeout !== null) { - window.clearTimeout(this.timeout); - } - - // Set a new timeout - this.timeout = window.setTimeout(() => { this.processKeyPresses(); }, 1000); - - } - - private processKeyPresses() { - this.keypresses = []; - } - - private listFuzzyMatch(str: string) { - for (const menuItem of this._allMenuItemsFiltered) { - if (menuItem.label) { - const result = matchesFuzzy2(str, menuItem.label); - // Result is either null or an array of obj {start, end} for matches between str and menuItem.label - if (result) { - this._list.setFocus([menuItem.index]); - if (result[0].start === 0) { - // Breaks when finds first found instance of match [0,1+] to [0,1+] - break; - } - } - } - } - } } function stripNewlines(str: string): string { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 1384b5f5e74..539af6ca265 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -94,7 +94,8 @@ .action-widget .monaco-list-row.action.option-disabled, .action-widget .monaco-list:focus .monaco-list-row.focused.action.option-disabled, -.action-widget .monaco-list-row.action.option-disabled .codicon { +.action-widget .monaco-list-row.action.option-disabled .codicon, +.action-widget .monaco-list:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused).option-disabled { color: var(--vscode-disabledForeground); } diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index cb6ab4c31df..4965d6bb5cd 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -55,7 +55,7 @@ export class MenuWorkbenchButtonBar extends ButtonBar { 'workbenchActionExecuted', { id: e.action.id, from: options.telemetrySource! } ); - }, this._store); + }, undefined, this._store); } const conifgProvider: IButtonConfigProvider = options?.buttonConfigProvider ?? (() => ({ showLabel: true })); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index d1f216f0971..0b6f1148464 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -187,6 +187,7 @@ export class MenuId { static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); static readonly ChatExecute = new MenuId('ChatExecute'); + static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly AccessibleView = new MenuId('AccessibleView'); /** diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 17476d94187..b63a262137f 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +export interface INativeCliOptions { + 'cli-data-dir'?: string; + 'disable-telemetry'?: boolean; + 'telemetry-level'?: string; +} + /** * A list of command line arguments we support natively. */ export interface NativeParsedArgs { // subcommands - tunnel?: { - 'cli-data-dir'?: string; - 'disable-telemetry'?: boolean; - 'telemetry-level'?: string; + tunnel?: INativeCliOptions & { user: { login: { 'access-token'?: string; @@ -19,6 +22,7 @@ export interface NativeParsedArgs { }; }; }; + 'serve-web'?: INativeCliOptions; _: string[]; 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index b3e423e20f9..63243fcf3a0 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -44,6 +44,8 @@ export type OptionDescriptions = { Subcommand }; +export const NATIVE_CLI_COMMANDS = ['tunnel', 'serve-web'] as const; + export const OPTIONS: OptionDescriptions> = { 'tunnel': { type: 'subcommand', @@ -66,6 +68,15 @@ export const OPTIONS: OptionDescriptions> = { } } }, + 'serve-web': { + type: 'subcommand', + description: 'Make the current machine accessible from vscode.dev or other machines through a secure tunnel', + options: { + 'cli-data-dir': { type: 'string', args: 'dir', description: localize('cliDataDir', "Directory where CLI metadata should be stored.") }, + 'disable-telemetry': { type: 'boolean' }, + 'telemetry-level': { type: 'string' }, + } + }, 'diff': { type: 'boolean', cat: 'o', alias: 'd', args: ['file', 'file'], description: localize('diff', "Compare two files with each other.") }, 'merge': { type: 'boolean', cat: 'o', alias: 'm', args: ['path1', 'path2', 'base', 'result'], description: localize('merge', "Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.") }, diff --git a/src/vs/platform/environment/node/argvHelper.ts b/src/vs/platform/environment/node/argvHelper.ts index 74a7369225d..d8cefb6df67 100644 --- a/src/vs/platform/environment/node/argvHelper.ts +++ b/src/vs/platform/environment/node/argvHelper.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { ErrorReporter, OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; +import { ErrorReporter, NATIVE_CLI_COMMANDS, OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): NativeParsedArgs { const onMultipleValues = (id: string, val: string) => { @@ -21,14 +21,14 @@ function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): Nativ }; const getSubcommandReporter = (command: string) => ({ onUnknownOption: (id: string) => { - if (command !== 'tunnel') { + if (!(NATIVE_CLI_COMMANDS as readonly string[]).includes(command)) { console.warn(localize('unknownSubCommandOption', "Warning: '{0}' is not in the list of known options for subcommand '{1}'", id, command)); } }, onMultipleValues, onEmptyValue, onDeprecatedOption, - getSubcommandReporter: command !== 'tunnel' ? getSubcommandReporter : undefined + getSubcommandReporter: (NATIVE_CLI_COMMANDS as readonly string[]).includes(command) ? getSubcommandReporter : undefined }); const errorReporter: ErrorReporter = { onUnknownOption: (id) => { diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 23f3a635495..829a9b679c9 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -16,7 +16,7 @@ import * as nls from 'vs/nls'; import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, - InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT + InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -35,6 +35,7 @@ export interface IInstallExtensionTask { readonly source: IGalleryExtension | URI; readonly operation: InstallOperation; readonly profileLocation: URI; + readonly options: InstallExtensionTaskOptions; readonly verificationStatus?: ExtensionVerificationStatus; run(): Promise; waitUntilTaskIsFinished(): Promise; @@ -248,7 +249,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl allInstallExtensionTasks.push({ task: installExtensionTask, manifest }); let installExtensionHasDependents: boolean = false; - const hasPackExtensions = manifest.extensionPack && manifest.extensionPack.length > 0; try { if (installExtensionTaskOptions.donotIncludePackAndDependencies) { this.logService.info('Installing the extension without checking dependencies and pack', installExtensionTask.identifier.id); @@ -256,6 +256,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl try { const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(installExtensionTask.identifier, manifest, !!installExtensionTaskOptions.installOnlyNewlyAddedFromExtensionPack, !!installExtensionTaskOptions.installPreReleaseVersion, installExtensionTaskOptions.profileLocation); const installed = await this.getInstalled(undefined, installExtensionTaskOptions.profileLocation); + const options: InstallExtensionTaskOptions = { ...installExtensionTaskOptions, donotIncludePackAndDependencies: true, context: { ...installExtensionTaskOptions.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier)); const key = getInstallExtensionTaskKey(gallery); @@ -279,7 +280,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } } else if (!installed.some(({ identifier }) => areSameExtensions(identifier, gallery.identifier))) { - const task = this.createInstallExtensionTask(manifest, gallery, { ...installExtensionTaskOptions, donotIncludePackAndDependencies: true }); + const task = this.createInstallExtensionTask(manifest, gallery, options); this.installingExtensions.set(key, { task, waitingTasks: [installExtensionTask] }); this._onInstallExtension.fire({ identifier: task.identifier, source: gallery, profileLocation: installExtensionTaskOptions.profileLocation }); this.logService.info('Installing extension:', task.identifier.id, installExtensionTask.identifier.id); @@ -343,12 +344,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } - const context = installExtensionTaskOptions.context ?? {}; - if (hasPackExtensions && task.identifier.id !== installExtensionTask.identifier.id) { - context[EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT] = true; - } - - installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); + installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); } catch (error) { if (!URI.isUri(task.source)) { reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 2fcc28f621f..9d6e9c4739a 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -26,6 +26,7 @@ import { asJson, asTextOrError, IRequestService, isSuccess } from 'vs/platform/r import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { StopWatch } from 'vs/base/common/stopwatch'; const CURRENT_TARGET_PLATFORM = isWeb ? TargetPlatform.WEB : getTargetPlatform(platform, arch); const ACTIVITY_HEADER_NAME = 'X-Market-Search-Activity-Id'; @@ -874,14 +875,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } if (needAllVersions.size) { - const startTime = new Date().getTime(); + const stopWatch = new StopWatch(); const query = new Query() .withFlags(flags & ~Flags.IncludeLatestVersionOnly, Flags.IncludeVersions) .withPage(1, needAllVersions.size) .withFilter(FilterType.ExtensionId, ...needAllVersions.keys()); const { extensions } = await this.queryGalleryExtensions(query, criteria, token); this.telemetryService.publicLog2('galleryService:additionalQuery', { - duration: new Date().getTime() - startTime, + duration: stopWatch.elapsed(), count: needAllVersions.size }); for (const extension of extensions) { @@ -952,7 +953,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi 'Content-Length': String(data.length), }; - const startTime = new Date().getTime(); + const stopWatch = new StopWatch(); let context: IRequestContext | undefined, error: any, total: number = 0; try { @@ -991,7 +992,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.telemetryService.publicLog2('galleryService:query', { ...query.telemetryData, requestBodySize: String(data.length), - duration: new Date().getTime() - startTime, + duration: stopWatch.elapsed(), success: !!context && isSuccess(context), responseBodySize: context?.res.headers['Content-Length'], statusCode: context ? String(context.res.statusCode) : undefined, diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 9a9abb281fc..dc4c1ccc02c 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -18,6 +18,7 @@ export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTER export const WEB_EXTENSION_TAG = '__web_extension'; export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; +export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 30e749ac312..40eb2584305 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -124,22 +124,24 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable const extensionsToRemove: IScannedProfileExtension[] = []; const extensionsToAdd: IScannedProfileExtension[] = []; try { - await this.withProfileExtensions(profileLocation, profileExtensions => { + await this.withProfileExtensions(profileLocation, existingExtensions => { const result: IScannedProfileExtension[] = []; - for (const extension of profileExtensions) { - if (extensions.some(([e]) => areSameExtensions(e.identifier, extension.identifier) && e.manifest.version !== extension.version)) { + for (const existing of existingExtensions) { + if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { // Remove the existing extension with different version - extensionsToRemove.push(extension); + extensionsToRemove.push(existing); } else { - result.push(extension); + result.push(existing); } } for (const [extension, metadata] of extensions) { - if (!result.some(e => areSameExtensions(e.identifier, extension.identifier) && e.version === extension.manifest.version)) { - // Add only if the same version of the extension is not already added - const extensionToAdd = { identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata }; + const index = result.findIndex(e => areSameExtensions(e.identifier, extension.identifier) && e.version === extension.manifest.version); + const extensionToAdd = { identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata }; + if (index === -1) { extensionsToAdd.push(extensionToAdd); result.push(extensionToAdd); + } else { + result.splice(index, 1, extensionToAdd); } } if (extensionsToAdd.length) { @@ -189,7 +191,6 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable async removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; - this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); try { await this.withProfileExtensions(profileLocation, profileExtensions => { const result: IScannedProfileExtension[] = []; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 6c7b92de0ef..677149f937b 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -782,7 +782,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { instantiationService.stub(IUserDataProfilesService, userDataProfilesService); }); + teardown(() => disposables.clear()); + + suiteTeardown(() => sinon.restore()); + test('write extensions located in the same extensions folder', async () => { const testObject = instantiationService.createInstance(TestObject, extensionsLocation); @@ -442,7 +447,247 @@ suite('ExtensionsProfileScannerService', () => { assert.deepStrictEqual(manifestContent, [{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version }]); }); - function aExtension(id: string, location: URI, e?: Partial): IExtension { + test('add extension trigger events', async () => { + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const target1 = sinon.stub(); + const target2 = sinon.stub(); + testObject.onAddExtensions(target1); + testObject.onDidAddExtensions(target2); + + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); + + assert.ok(target1.calledOnce); + assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + + assert.ok(target2.calledOnce); + assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + }); + + test('remove extension trigger events', async () => { + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + const target1 = sinon.stub(); + const target2 = sinon.stub(); + testObject.onRemoveExtensions(target1); + testObject.onDidRemoveExtensions(target2); + + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); + await testObject.removeExtensionFromProfile(extension, extensionsManifest); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.length, 0); + + assert.ok(target1.calledOnce); + assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + + assert.ok(target2.calledOnce); + assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + }); + + test('add extension with same id but different version', async () => { + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + + const extension1 = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension1, undefined]], extensionsManifest); + + const target1 = sinon.stub(); + const target2 = sinon.stub(); + const target3 = sinon.stub(); + const target4 = sinon.stub(); + testObject.onAddExtensions(target1); + testObject.onRemoveExtensions(target2); + testObject.onDidAddExtensions(target3); + testObject.onDidRemoveExtensions(target4); + const extension2 = aExtension('pub.a', joinPath(ROOT, 'pub.a-2.0.0'), undefined, { version: '2.0.0' }); + await testObject.addExtensionsToProfile([[extension2, undefined]], extensionsManifest); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: undefined }]); + + assert.ok(target1.calledOnce); + assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension2.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension2.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString()); + + assert.ok(target2.calledOnce); + assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + + assert.ok(target3.calledOnce); + assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension2.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension2.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString()); + + assert.ok(target4.calledOnce); + assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + }); + + test('add same extension', async () => { + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + + const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); + + const target1 = sinon.stub(); + const target2 = sinon.stub(); + const target3 = sinon.stub(); + const target4 = sinon.stub(); + testObject.onAddExtensions(target1); + testObject.onRemoveExtensions(target2); + testObject.onDidAddExtensions(target3); + testObject.onDidRemoveExtensions(target4); + await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); + assert.ok(target1.notCalled); + assert.ok(target2.notCalled); + assert.ok(target3.notCalled); + assert.ok(target4.notCalled); + }); + + test('add same extension with different metadata', async () => { + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + + const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); + + const target1 = sinon.stub(); + const target2 = sinon.stub(); + const target3 = sinon.stub(); + const target4 = sinon.stub(); + testObject.onAddExtensions(target1); + testObject.onRemoveExtensions(target2); + testObject.onDidAddExtensions(target3); + testObject.onDidRemoveExtensions(target4); + await testObject.addExtensionsToProfile([[extension, { isApplicationScoped: true }]], extensionsManifest); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON(), metadata: a.metadata })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: { isApplicationScoped: true } }]); + assert.ok(target1.notCalled); + assert.ok(target2.notCalled); + assert.ok(target3.notCalled); + assert.ok(target4.notCalled); + }); + + test('add extension with different version and metadata', async () => { + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + + const extension1 = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension1, undefined]], extensionsManifest); + const extension2 = aExtension('pub.a', joinPath(ROOT, 'pub.a-2.0.0'), undefined, { version: '2.0.0' }); + + const target1 = sinon.stub(); + const target2 = sinon.stub(); + const target3 = sinon.stub(); + const target4 = sinon.stub(); + testObject.onAddExtensions(target1); + testObject.onRemoveExtensions(target2); + testObject.onDidAddExtensions(target3); + testObject.onDidRemoveExtensions(target4); + await testObject.addExtensionsToProfile([[extension2, { isApplicationScoped: true }]], extensionsManifest); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON(), metadata: a.metadata })), [{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: { isApplicationScoped: true } }]); + + assert.ok(target1.calledOnce); + assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension2.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension2.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString()); + + assert.ok(target2.calledOnce); + assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + + assert.ok(target3.calledOnce); + assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension2.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension2.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString()); + + assert.ok(target4.calledOnce); + assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + }); + + test('add extension with same id and version located in the different folder', async () => { + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + + let extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); + + const target1 = sinon.stub(); + const target2 = sinon.stub(); + const target3 = sinon.stub(); + const target4 = sinon.stub(); + testObject.onAddExtensions(target1); + testObject.onRemoveExtensions(target2); + testObject.onDidAddExtensions(target3); + testObject.onDidRemoveExtensions(target4); + extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0')); + await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); + assert.ok(target1.notCalled); + assert.ok(target2.notCalled); + assert.ok(target3.notCalled); + assert.ok(target4.notCalled); + }); + + function aExtension(id: string, location: URI, e?: Partial, manifest?: Partial): IExtension { return { identifier: { id }, location, @@ -454,6 +699,7 @@ suite('ExtensionsProfileScannerService', () => { publisher: 'publisher', version: '1.0.0', engines: { vscode: '1.0.0' }, + ...manifest, }, isValid: true, validations: [], diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 4f6affdefbb..c56674f2f78 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -6,7 +6,6 @@ import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { CountBadge, ICountBadgeStyles } from 'vs/base/browser/ui/countBadge/countBadge'; import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; @@ -19,19 +18,16 @@ import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Action } from 'vs/base/common/actions'; import { equals } from 'vs/base/common/arrays'; import { TimeoutTimer } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isIOS } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { ThemeIcon } from 'vs/base/common/themables'; -import { isString } from 'vs/base/common/types'; import 'vs/css!./media/quickInput'; import { localize } from 'vs/nls'; -import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, ItemActivation, NO_KEY_MODS, QuickInputHideReason, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from './quickInputBox'; import { QuickInputList, QuickInputListFocus } from './quickInputList'; import { getIconClass, renderQuickInputDescription } from './quickInputUtils'; @@ -77,17 +73,15 @@ export interface IQuickInputWidgetStyles { readonly widgetShadow: string | undefined; } -const $ = dom.$; +export type Writeable = { -readonly [P in keyof T]: T[P] }; -type Writeable = { -readonly [P in keyof T]: T[P] }; - -const backButton = { +export const backButton = { iconClass: ThemeIcon.asClassName(Codicon.quickInputBack), tooltip: localize('quickInput.back', "Back"), handle: -1 // TODO }; -interface QuickInputUI { +export interface QuickInputUI { container: HTMLElement; styleSheet: HTMLStyleElement; leftActionBar: ActionBar; @@ -125,7 +119,7 @@ interface QuickInputUI { hide(): void; } -type Visibilities = { +export type Visibilities = { title?: boolean; description?: boolean; checkAll?: boolean; @@ -496,7 +490,7 @@ class QuickInput extends Disposable implements IQuickInput { } } -class QuickPick extends QuickInput implements IQuickPick { +export class QuickPick extends QuickInput implements IQuickPick { private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); @@ -1155,7 +1149,7 @@ class QuickPick extends QuickInput implements IQuickPi } } -class InputBox extends QuickInput implements IInputBox { +export class InputBox extends QuickInput implements IInputBox { private _value = ''; private _valueSelection: Readonly<[number, number]> | undefined; private valueSelectionUpdated = true; @@ -1262,704 +1256,18 @@ class InputBox extends QuickInput implements IInputBox { } } -export class QuickInputController extends Disposable { - private static readonly MAX_WIDTH = 600; // Max total width of quick input widget - - private idPrefix: string; - private ui: QuickInputUI | undefined; - private dimension?: dom.IDimension; - private titleBarOffset?: number; - private enabled = true; - private readonly onDidAcceptEmitter = this._register(new Emitter()); - private readonly onDidCustomEmitter = this._register(new Emitter()); - private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); - private keyMods: Writeable = { ctrlCmd: false, alt: false }; - - private controller: QuickInput | null = null; - - private parentElement: HTMLElement; - private styles: IQuickInputStyles; - - private onShowEmitter = this._register(new Emitter()); - readonly onShow = this.onShowEmitter.event; - - private onHideEmitter = this._register(new Emitter()); - readonly onHide = this.onHideEmitter.event; - - private previousFocusElement?: HTMLElement; - - constructor(private options: IQuickInputOptions) { - super(); - this.idPrefix = options.idPrefix; - this.parentElement = options.container; - this.styles = options.styles; - this.registerKeyModsListeners(); - } - - private registerKeyModsListeners() { - const listener = (e: KeyboardEvent | MouseEvent) => { - this.keyMods.ctrlCmd = e.ctrlKey || e.metaKey; - this.keyMods.alt = e.altKey; - }; - this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, listener, true)); - this._register(dom.addDisposableListener(window, dom.EventType.KEY_UP, listener, true)); - this._register(dom.addDisposableListener(window, dom.EventType.MOUSE_DOWN, listener, true)); - } - - private getUI() { - if (this.ui) { - return this.ui; - } - - const container = dom.append(this.parentElement, $('.quick-input-widget.show-file-icons')); - container.tabIndex = -1; - container.style.display = 'none'; - - const styleSheet = dom.createStyleSheet(container); - - const titleBar = dom.append(container, $('.quick-input-titlebar')); - - const leftActionBar = this._register(new ActionBar(titleBar)); - leftActionBar.domNode.classList.add('quick-input-left-action-bar'); - - const title = dom.append(titleBar, $('.quick-input-title')); - - const rightActionBar = this._register(new ActionBar(titleBar)); - rightActionBar.domNode.classList.add('quick-input-right-action-bar'); - - const headerContainer = dom.append(container, $('.quick-input-header')); - - const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); - checkAll.type = 'checkbox'; - checkAll.setAttribute('aria-label', localize('quickInput.checkAll', "Toggle all checkboxes")); - this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { - const checked = checkAll.checked; - list.setAllVisibleChecked(checked); - })); - this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { - if (e.x || e.y) { // Avoid 'click' triggered by 'space'... - inputBox.setFocus(); - } - })); - - const description2 = dom.append(headerContainer, $('.quick-input-description')); - const inputContainer = dom.append(headerContainer, $('.quick-input-and-message')); - const filterContainer = dom.append(inputContainer, $('.quick-input-filter')); - - const inputBox = this._register(new QuickInputBox(filterContainer, this.styles.inputBox, this.styles.toggle)); - inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`); - - const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count')); - visibleCountContainer.setAttribute('aria-live', 'polite'); - visibleCountContainer.setAttribute('aria-atomic', 'true'); - const visibleCount = new CountBadge(visibleCountContainer, { countFormat: localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results") }, this.styles.countBadge); - - const countContainer = dom.append(filterContainer, $('.quick-input-count')); - countContainer.setAttribute('aria-live', 'polite'); - const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }, this.styles.countBadge); - - const okContainer = dom.append(headerContainer, $('.quick-input-action')); - const ok = new Button(okContainer, this.styles.button); - ok.label = localize('ok', "OK"); - this._register(ok.onDidClick(e => { - this.onDidAcceptEmitter.fire(); - })); - - const customButtonContainer = dom.append(headerContainer, $('.quick-input-action')); - const customButton = new Button(customButtonContainer, this.styles.button); - customButton.label = localize('custom', "Custom"); - this._register(customButton.onDidClick(e => { - this.onDidCustomEmitter.fire(); - })); - - const message = dom.append(inputContainer, $(`#${this.idPrefix}message.quick-input-message`)); - - const progressBar = new ProgressBar(container, this.styles.progressBar); - progressBar.getContainer().classList.add('quick-input-progress'); - - const widget = dom.append(container, $('.quick-input-html-widget')); - widget.tabIndex = -1; - - const description1 = dom.append(container, $('.quick-input-description')); - - const listId = this.idPrefix + 'list'; - const list = this._register(new QuickInputList(container, listId, this.options)); - inputBox.setAttribute('aria-controls', listId); - this._register(list.onDidChangeFocus(() => { - inputBox.setAttribute('aria-activedescendant', list.getActiveDescendant() ?? ''); - })); - this._register(list.onChangedAllVisibleChecked(checked => { - checkAll.checked = checked; - })); - this._register(list.onChangedVisibleCount(c => { - visibleCount.setCount(c); - })); - this._register(list.onChangedCheckedCount(c => { - count.setCount(c); - })); - this._register(list.onLeave(() => { - // Defer to avoid the input field reacting to the triggering key. - setTimeout(() => { - inputBox.setFocus(); - if (this.controller instanceof QuickPick && this.controller.canSelectMany) { - list.clearFocus(); - } - }, 0); - })); - - const focusTracker = dom.trackFocus(container); - this._register(focusTracker); - this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, e => { - // Ignore focus events within container - if (dom.isAncestor(e.relatedTarget as HTMLElement, container)) { - return; - } - this.previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined; - }, true)); - this._register(focusTracker.onDidBlur(() => { - if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) { - this.hide(QuickInputHideReason.Blur); - } - this.previousFocusElement = undefined; - })); - this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, (e: FocusEvent) => { - inputBox.setFocus(); - })); - // TODO: Turn into commands instead of handling KEY_DOWN - this._register(dom.addStandardDisposableListener(container, dom.EventType.KEY_DOWN, (event) => { - if (dom.isAncestor(event.target, widget)) { - return; // Ignore event if target is inside widget to allow the widget to handle the event. - } - switch (event.keyCode) { - case KeyCode.Enter: - dom.EventHelper.stop(event, true); - if (this.enabled) { - this.onDidAcceptEmitter.fire(); - } - break; - case KeyCode.Escape: - dom.EventHelper.stop(event, true); - this.hide(QuickInputHideReason.Gesture); - break; - case KeyCode.Tab: - if (!event.altKey && !event.ctrlKey && !event.metaKey) { - // detect only visible actions - const selectors = [ - '.quick-input-list .monaco-action-bar .always-visible', - '.quick-input-list-entry:hover .monaco-action-bar', - '.monaco-list-row.focused .monaco-action-bar' - ]; - - if (container.classList.contains('show-checkboxes')) { - selectors.push('input'); - } else { - selectors.push('input[type=text]'); - } - if (this.getUI().list.isDisplayed()) { - selectors.push('.monaco-list'); - } - // focus links if there are any - if (this.getUI().message) { - selectors.push('.quick-input-message a'); - } - - if (this.getUI().widget) { - if (dom.isAncestor(event.target, this.getUI().widget)) { - // let the widget control tab - break; - } - selectors.push('.quick-input-html-widget'); - } - const stops = container.querySelectorAll(selectors.join(', ')); - if (event.shiftKey && event.target === stops[0]) { - // Clear the focus from the list in order to allow - // screen readers to read operations in the input box. - dom.EventHelper.stop(event, true); - list.clearFocus(); - } else if (!event.shiftKey && dom.isAncestor(event.target, stops[stops.length - 1])) { - dom.EventHelper.stop(event, true); - stops[0].focus(); - } - } - break; - case KeyCode.Space: - if (event.ctrlKey) { - dom.EventHelper.stop(event, true); - this.getUI().list.toggleHover(); - } - break; - } - })); - - this.ui = { - container, - styleSheet, - leftActionBar, - titleBar, - title, - description1, - description2, - widget, - rightActionBar, - checkAll, - inputContainer, - filterContainer, - inputBox, - visibleCountContainer, - visibleCount, - countContainer, - count, - okContainer, - ok, - message, - customButtonContainer, - customButton, - list, - progressBar, - onDidAccept: this.onDidAcceptEmitter.event, - onDidCustom: this.onDidCustomEmitter.event, - onDidTriggerButton: this.onDidTriggerButtonEmitter.event, - ignoreFocusOut: false, - keyMods: this.keyMods, - show: controller => this.show(controller), - hide: () => this.hide(), - setVisibilities: visibilities => this.setVisibilities(visibilities), - setEnabled: enabled => this.setEnabled(enabled), - setContextKey: contextKey => this.options.setContextKey(contextKey), - linkOpenerDelegate: content => this.options.linkOpenerDelegate(content) - }; - this.updateStyles(); - return this.ui; - } - - pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { - type R = (O extends { canPickMany: true } ? T[] : T) | undefined; - return new Promise((doResolve, reject) => { - let resolve = (result: R) => { - resolve = doResolve; - options.onKeyMods?.(input.keyMods); - doResolve(result); - }; - if (token.isCancellationRequested) { - resolve(undefined); - return; - } - const input = this.createQuickPick(); - let activeItem: T | undefined; - const disposables = [ - input, - input.onDidAccept(() => { - if (input.canSelectMany) { - resolve(input.selectedItems.slice()); - input.hide(); - } else { - const result = input.activeItems[0]; - if (result) { - resolve(result); - input.hide(); - } - } - }), - input.onDidChangeActive(items => { - const focused = items[0]; - if (focused && options.onDidFocus) { - options.onDidFocus(focused); - } - }), - input.onDidChangeSelection(items => { - if (!input.canSelectMany) { - const result = items[0]; - if (result) { - resolve(result); - input.hide(); - } - } - }), - input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({ - ...event, - removeItem: () => { - const index = input.items.indexOf(event.item); - if (index !== -1) { - const items = input.items.slice(); - const removed = items.splice(index, 1); - const activeItems = input.activeItems.filter(activeItem => activeItem !== removed[0]); - const keepScrollPositionBefore = input.keepScrollPosition; - input.keepScrollPosition = true; - input.items = items; - if (activeItems) { - input.activeItems = activeItems; - } - input.keepScrollPosition = keepScrollPositionBefore; - } - } - })), - input.onDidTriggerSeparatorButton(event => options.onDidTriggerSeparatorButton?.(event)), - input.onDidChangeValue(value => { - if (activeItem && !value && (input.activeItems.length !== 1 || input.activeItems[0] !== activeItem)) { - input.activeItems = [activeItem]; - } - }), - token.onCancellationRequested(() => { - input.hide(); - }), - input.onDidHide(() => { - dispose(disposables); - resolve(undefined); - }), - ]; - input.title = options.title; - input.canSelectMany = !!options.canPickMany; - input.placeholder = options.placeHolder; - input.ignoreFocusOut = !!options.ignoreFocusLost; - input.matchOnDescription = !!options.matchOnDescription; - input.matchOnDetail = !!options.matchOnDetail; - input.matchOnLabel = (options.matchOnLabel === undefined) || options.matchOnLabel; // default to true - input.autoFocusOnList = (options.autoFocusOnList === undefined) || options.autoFocusOnList; // default to true - input.quickNavigate = options.quickNavigate; - input.hideInput = !!options.hideInput; - input.contextKey = options.contextKey; - input.busy = true; - Promise.all([picks, options.activeItem]) - .then(([items, _activeItem]) => { - activeItem = _activeItem; - input.busy = false; - input.items = items; - if (input.canSelectMany) { - input.selectedItems = items.filter(item => item.type !== 'separator' && item.picked) as T[]; - } - if (activeItem) { - input.activeItems = [activeItem]; - } - }); - input.show(); - Promise.resolve(picks).then(undefined, err => { - reject(err); - input.hide(); - }); - }); - } - - private setValidationOnInput(input: IInputBox, validationResult: string | { - content: string; - severity: Severity; - } | null | undefined) { - if (validationResult && isString(validationResult)) { - input.severity = Severity.Error; - input.validationMessage = validationResult; - } else if (validationResult && !isString(validationResult)) { - input.severity = validationResult.severity; - input.validationMessage = validationResult.content; - } else { - input.severity = Severity.Ignore; - input.validationMessage = undefined; - } - } - - input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): Promise { - return new Promise((resolve) => { - if (token.isCancellationRequested) { - resolve(undefined); - return; - } - const input = this.createInputBox(); - const validateInput = options.validateInput || (() => >Promise.resolve(undefined)); - const onDidValueChange = Event.debounce(input.onDidChangeValue, (last, cur) => cur, 100); - let validationValue = options.value || ''; - let validation = Promise.resolve(validateInput(validationValue)); - const disposables = [ - input, - onDidValueChange(value => { - if (value !== validationValue) { - validation = Promise.resolve(validateInput(value)); - validationValue = value; - } - validation.then(result => { - if (value === validationValue) { - this.setValidationOnInput(input, result); - } - }); - }), - input.onDidAccept(() => { - const value = input.value; - if (value !== validationValue) { - validation = Promise.resolve(validateInput(value)); - validationValue = value; - } - validation.then(result => { - if (!result || (!isString(result) && result.severity !== Severity.Error)) { - resolve(value); - input.hide(); - } else if (value === validationValue) { - this.setValidationOnInput(input, result); - } - }); - }), - token.onCancellationRequested(() => { - input.hide(); - }), - input.onDidHide(() => { - dispose(disposables); - resolve(undefined); - }), - ]; - - input.title = options.title; - input.value = options.value || ''; - input.valueSelection = options.valueSelection; - input.prompt = options.prompt; - input.placeholder = options.placeHolder; - input.password = !!options.password; - input.ignoreFocusOut = !!options.ignoreFocusLost; - input.show(); - }); - } - - backButton = backButton; - - createQuickPick(): IQuickPick { - const ui = this.getUI(); - return new QuickPick(ui); - } - - createInputBox(): IInputBox { - const ui = this.getUI(); - return new InputBox(ui); - } - - private show(controller: QuickInput) { - const ui = this.getUI(); - this.onShowEmitter.fire(); - const oldController = this.controller; - this.controller = controller; - oldController?.didHide(); - - this.setEnabled(true); - ui.leftActionBar.clear(); - ui.title.textContent = ''; - ui.description1.textContent = ''; - ui.description2.textContent = ''; - dom.reset(ui.widget); - ui.rightActionBar.clear(); - ui.checkAll.checked = false; - // ui.inputBox.value = ''; Avoid triggering an event. - ui.inputBox.placeholder = ''; - ui.inputBox.password = false; - ui.inputBox.showDecoration(Severity.Ignore); - ui.visibleCount.setCount(0); - ui.count.setCount(0); - dom.reset(ui.message); - ui.progressBar.stop(); - ui.list.setElements([]); - ui.list.matchOnDescription = false; - ui.list.matchOnDetail = false; - ui.list.matchOnLabel = true; - ui.list.sortByLabel = true; - ui.ignoreFocusOut = false; - ui.inputBox.toggles = undefined; - - const backKeybindingLabel = this.options.backKeybindingLabel(); - backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); - - ui.container.style.display = ''; - this.updateLayout(); - ui.inputBox.setFocus(); - } - - private setVisibilities(visibilities: Visibilities) { - const ui = this.getUI(); - ui.title.style.display = visibilities.title ? '' : 'none'; - ui.description1.style.display = visibilities.description && (visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; - ui.description2.style.display = visibilities.description && !(visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; - ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; - ui.inputContainer.style.display = visibilities.inputBox ? '' : 'none'; - ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; - ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; - ui.countContainer.style.display = visibilities.count ? '' : 'none'; - ui.okContainer.style.display = visibilities.ok ? '' : 'none'; - ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; - ui.message.style.display = visibilities.message ? '' : 'none'; - ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none'; - ui.list.display(!!visibilities.list); - ui.container.classList.toggle('show-checkboxes', !!visibilities.checkBox); - ui.container.classList.toggle('hidden-input', !visibilities.inputBox && !visibilities.description); - this.updateLayout(); // TODO - } - - private setEnabled(enabled: boolean) { - if (enabled !== this.enabled) { - this.enabled = enabled; - for (const item of this.getUI().leftActionBar.viewItems) { - (item as ActionViewItem).action.enabled = enabled; - } - for (const item of this.getUI().rightActionBar.viewItems) { - (item as ActionViewItem).action.enabled = enabled; - } - this.getUI().checkAll.disabled = !enabled; - this.getUI().inputBox.enabled = enabled; - this.getUI().ok.enabled = enabled; - this.getUI().list.enabled = enabled; - } - } - - hide(reason?: QuickInputHideReason) { - const controller = this.controller; - if (!controller) { +export class QuickWidget extends QuickInput implements IQuickWidget { + protected override update() { + if (!this.visible) { return; } - const focusChanged = !dom.isAncestor(document.activeElement, this.ui?.container ?? null); - this.controller = null; - this.onHideEmitter.fire(); - this.getUI().container.style.display = 'none'; - if (!focusChanged) { - let currentElement = this.previousFocusElement; - while (currentElement && !currentElement.offsetParent) { - currentElement = currentElement.parentElement ?? undefined; - } - if (currentElement?.offsetParent) { - currentElement.focus(); - this.previousFocusElement = undefined; - } else { - this.options.returnFocus(); - } - } - controller.didHide(reason); - } + const visibilities: Visibilities = { + title: !!this.title || !!this.step || !!this.buttons.length, + description: !!this.description || !!this.step + }; - focus() { - if (this.isDisplayed()) { - const ui = this.getUI(); - if (ui.inputBox.enabled) { - ui.inputBox.setFocus(); - } else { - ui.list.domFocus(); - } - } - } - - toggle() { - if (this.isDisplayed() && this.controller instanceof QuickPick && this.controller.canSelectMany) { - this.getUI().list.toggleCheckbox(); - } - } - - navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { - if (this.isDisplayed() && this.getUI().list.isDisplayed()) { - this.getUI().list.focus(next ? QuickInputListFocus.Next : QuickInputListFocus.Previous); - if (quickNavigate && this.controller instanceof QuickPick) { - this.controller.quickNavigate = quickNavigate; - } - } - } - - async accept(keyMods: IKeyMods = { alt: false, ctrlCmd: false }) { - // When accepting the item programmatically, it is important that - // we update `keyMods` either from the provided set or unset it - // because the accept did not happen from mouse or keyboard - // interaction on the list itself - this.keyMods.alt = keyMods.alt; - this.keyMods.ctrlCmd = keyMods.ctrlCmd; - - this.onDidAcceptEmitter.fire(); - } - - async back() { - this.onDidTriggerButtonEmitter.fire(this.backButton); - } - - async cancel() { - this.hide(); - } - - layout(dimension: dom.IDimension, titleBarOffset: number): void { - this.dimension = dimension; - this.titleBarOffset = titleBarOffset; - this.updateLayout(); - } - - private updateLayout() { - if (this.ui && this.isDisplayed()) { - this.ui.container.style.top = `${this.titleBarOffset}px`; - - const style = this.ui.container.style; - const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); - style.width = width + 'px'; - style.marginLeft = '-' + (width / 2) + 'px'; - - this.ui.inputBox.layout(); - this.ui.list.layout(this.dimension && this.dimension.height * 0.4); - } - } - - applyStyles(styles: IQuickInputStyles) { - this.styles = styles; - this.updateStyles(); - } - - private updateStyles() { - if (this.ui) { - const { - quickInputTitleBackground, - quickInputBackground, - quickInputForeground, - widgetBorder, - widgetShadow, - } = this.styles.widget; - this.ui.titleBar.style.backgroundColor = quickInputTitleBackground ?? ''; - this.ui.container.style.backgroundColor = quickInputBackground ?? ''; - this.ui.container.style.color = quickInputForeground ?? ''; - this.ui.container.style.border = widgetBorder ? `1px solid ${widgetBorder}` : ''; - this.ui.container.style.boxShadow = widgetShadow ? `0 0 8px 2px ${widgetShadow}` : ''; - this.ui.list.style(this.styles.list); - - const content: string[] = []; - if (this.styles.pickerGroup.pickerGroupBorder) { - content.push(`.quick-input-list .quick-input-list-entry { border-top-color: ${this.styles.pickerGroup.pickerGroupBorder}; }`); - } - if (this.styles.pickerGroup.pickerGroupForeground) { - content.push(`.quick-input-list .quick-input-list-separator { color: ${this.styles.pickerGroup.pickerGroupForeground}; }`); - } - if (this.styles.pickerGroup.pickerGroupForeground) { - content.push(`.quick-input-list .quick-input-list-separator-as-item { color: ${this.styles.pickerGroup.pickerGroupForeground}; }`); - } - - if ( - this.styles.keybindingLabel.keybindingLabelBackground || - this.styles.keybindingLabel.keybindingLabelBorder || - this.styles.keybindingLabel.keybindingLabelBottomBorder || - this.styles.keybindingLabel.keybindingLabelShadow || - this.styles.keybindingLabel.keybindingLabelForeground - ) { - content.push('.quick-input-list .monaco-keybinding > .monaco-keybinding-key {'); - if (this.styles.keybindingLabel.keybindingLabelBackground) { - content.push(`background-color: ${this.styles.keybindingLabel.keybindingLabelBackground};`); - } - if (this.styles.keybindingLabel.keybindingLabelBorder) { - // Order matters here. `border-color` must come before `border-bottom-color`. - content.push(`border-color: ${this.styles.keybindingLabel.keybindingLabelBorder};`); - } - if (this.styles.keybindingLabel.keybindingLabelBottomBorder) { - content.push(`border-bottom-color: ${this.styles.keybindingLabel.keybindingLabelBottomBorder};`); - } - if (this.styles.keybindingLabel.keybindingLabelShadow) { - content.push(`box-shadow: inset 0 -1px 0 ${this.styles.keybindingLabel.keybindingLabelShadow};`); - } - if (this.styles.keybindingLabel.keybindingLabelForeground) { - content.push(`color: ${this.styles.keybindingLabel.keybindingLabelForeground};`); - } - content.push('}'); - } - - const newStyles = content.join('\n'); - if (newStyles !== this.ui.styleSheet.textContent) { - this.ui.styleSheet.textContent = newStyles; - } - } - } - - private isDisplayed() { - return this.ui && this.ui.container.style.display !== 'none'; + this.ui.setVisibilities(visibilities); + super.update(); } } - -export interface IQuickInputControllerHost extends ILayoutService { } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts new file mode 100644 index 00000000000..0328a6a6ded --- /dev/null +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -0,0 +1,726 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import Severity from 'vs/base/common/severity'; +import { isString } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { QuickInputBox } from 'vs/platform/quickinput/browser/quickInputBox'; +import { QuickInputList, QuickInputListFocus } from 'vs/platform/quickinput/browser/quickInputList'; +import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget } from 'vs/platform/quickinput/browser/quickInput'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; + +const $ = dom.$; + +export class QuickInputController extends Disposable { + private static readonly MAX_WIDTH = 600; // Max total width of quick input widget + + private idPrefix: string; + private ui: QuickInputUI | undefined; + private dimension?: dom.IDimension; + private titleBarOffset?: number; + private enabled = true; + private readonly onDidAcceptEmitter = this._register(new Emitter()); + private readonly onDidCustomEmitter = this._register(new Emitter()); + private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); + private keyMods: Writeable = { ctrlCmd: false, alt: false }; + + private controller: IQuickInput | null = null; + + private parentElement: HTMLElement; + private styles: IQuickInputStyles; + + private onShowEmitter = this._register(new Emitter()); + readonly onShow = this.onShowEmitter.event; + + private onHideEmitter = this._register(new Emitter()); + readonly onHide = this.onHideEmitter.event; + + private previousFocusElement?: HTMLElement; + + constructor(private options: IQuickInputOptions) { + super(); + this.idPrefix = options.idPrefix; + this.parentElement = options.container; + this.styles = options.styles; + this.registerKeyModsListeners(); + } + + private registerKeyModsListeners() { + const listener = (e: KeyboardEvent | MouseEvent) => { + this.keyMods.ctrlCmd = e.ctrlKey || e.metaKey; + this.keyMods.alt = e.altKey; + }; + this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, listener, true)); + this._register(dom.addDisposableListener(window, dom.EventType.KEY_UP, listener, true)); + this._register(dom.addDisposableListener(window, dom.EventType.MOUSE_DOWN, listener, true)); + } + + private getUI() { + if (this.ui) { + return this.ui; + } + + const container = dom.append(this.parentElement, $('.quick-input-widget.show-file-icons')); + container.tabIndex = -1; + container.style.display = 'none'; + + const styleSheet = dom.createStyleSheet(container); + + const titleBar = dom.append(container, $('.quick-input-titlebar')); + + const leftActionBar = this._register(new ActionBar(titleBar)); + leftActionBar.domNode.classList.add('quick-input-left-action-bar'); + + const title = dom.append(titleBar, $('.quick-input-title')); + + const rightActionBar = this._register(new ActionBar(titleBar)); + rightActionBar.domNode.classList.add('quick-input-right-action-bar'); + + const headerContainer = dom.append(container, $('.quick-input-header')); + + const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); + checkAll.type = 'checkbox'; + checkAll.setAttribute('aria-label', localize('quickInput.checkAll', "Toggle all checkboxes")); + this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { + const checked = checkAll.checked; + list.setAllVisibleChecked(checked); + })); + this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { + if (e.x || e.y) { // Avoid 'click' triggered by 'space'... + inputBox.setFocus(); + } + })); + + const description2 = dom.append(headerContainer, $('.quick-input-description')); + const inputContainer = dom.append(headerContainer, $('.quick-input-and-message')); + const filterContainer = dom.append(inputContainer, $('.quick-input-filter')); + + const inputBox = this._register(new QuickInputBox(filterContainer, this.styles.inputBox, this.styles.toggle)); + inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`); + + const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count')); + visibleCountContainer.setAttribute('aria-live', 'polite'); + visibleCountContainer.setAttribute('aria-atomic', 'true'); + const visibleCount = new CountBadge(visibleCountContainer, { countFormat: localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results") }, this.styles.countBadge); + + const countContainer = dom.append(filterContainer, $('.quick-input-count')); + countContainer.setAttribute('aria-live', 'polite'); + const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }, this.styles.countBadge); + + const okContainer = dom.append(headerContainer, $('.quick-input-action')); + const ok = new Button(okContainer, this.styles.button); + ok.label = localize('ok', "OK"); + this._register(ok.onDidClick(e => { + this.onDidAcceptEmitter.fire(); + })); + + const customButtonContainer = dom.append(headerContainer, $('.quick-input-action')); + const customButton = new Button(customButtonContainer, this.styles.button); + customButton.label = localize('custom', "Custom"); + this._register(customButton.onDidClick(e => { + this.onDidCustomEmitter.fire(); + })); + + const message = dom.append(inputContainer, $(`#${this.idPrefix}message.quick-input-message`)); + + const progressBar = new ProgressBar(container, this.styles.progressBar); + progressBar.getContainer().classList.add('quick-input-progress'); + + const widget = dom.append(container, $('.quick-input-html-widget')); + widget.tabIndex = -1; + + const description1 = dom.append(container, $('.quick-input-description')); + + const listId = this.idPrefix + 'list'; + const list = this._register(new QuickInputList(container, listId, this.options)); + inputBox.setAttribute('aria-controls', listId); + this._register(list.onDidChangeFocus(() => { + inputBox.setAttribute('aria-activedescendant', list.getActiveDescendant() ?? ''); + })); + this._register(list.onChangedAllVisibleChecked(checked => { + checkAll.checked = checked; + })); + this._register(list.onChangedVisibleCount(c => { + visibleCount.setCount(c); + })); + this._register(list.onChangedCheckedCount(c => { + count.setCount(c); + })); + this._register(list.onLeave(() => { + // Defer to avoid the input field reacting to the triggering key. + setTimeout(() => { + inputBox.setFocus(); + if (this.controller instanceof QuickPick && this.controller.canSelectMany) { + list.clearFocus(); + } + }, 0); + })); + + const focusTracker = dom.trackFocus(container); + this._register(focusTracker); + this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, e => { + // Ignore focus events within container + if (dom.isAncestor(e.relatedTarget as HTMLElement, container)) { + return; + } + this.previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined; + }, true)); + this._register(focusTracker.onDidBlur(() => { + if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) { + this.hide(QuickInputHideReason.Blur); + } + this.previousFocusElement = undefined; + })); + this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, (e: FocusEvent) => { + inputBox.setFocus(); + })); + // TODO: Turn into commands instead of handling KEY_DOWN + this._register(dom.addStandardDisposableListener(container, dom.EventType.KEY_DOWN, (event) => { + if (dom.isAncestor(event.target, widget)) { + return; // Ignore event if target is inside widget to allow the widget to handle the event. + } + switch (event.keyCode) { + case KeyCode.Enter: + dom.EventHelper.stop(event, true); + if (this.enabled) { + this.onDidAcceptEmitter.fire(); + } + break; + case KeyCode.Escape: + dom.EventHelper.stop(event, true); + this.hide(QuickInputHideReason.Gesture); + break; + case KeyCode.Tab: + if (!event.altKey && !event.ctrlKey && !event.metaKey) { + // detect only visible actions + const selectors = [ + '.quick-input-list .monaco-action-bar .always-visible', + '.quick-input-list-entry:hover .monaco-action-bar', + '.monaco-list-row.focused .monaco-action-bar' + ]; + + if (container.classList.contains('show-checkboxes')) { + selectors.push('input'); + } else { + selectors.push('input[type=text]'); + } + if (this.getUI().list.isDisplayed()) { + selectors.push('.monaco-list'); + } + // focus links if there are any + if (this.getUI().message) { + selectors.push('.quick-input-message a'); + } + + if (this.getUI().widget) { + if (dom.isAncestor(event.target, this.getUI().widget)) { + // let the widget control tab + break; + } + selectors.push('.quick-input-html-widget'); + } + const stops = container.querySelectorAll(selectors.join(', ')); + if (event.shiftKey && event.target === stops[0]) { + // Clear the focus from the list in order to allow + // screen readers to read operations in the input box. + dom.EventHelper.stop(event, true); + list.clearFocus(); + } else if (!event.shiftKey && dom.isAncestor(event.target, stops[stops.length - 1])) { + dom.EventHelper.stop(event, true); + stops[0].focus(); + } + } + break; + case KeyCode.Space: + if (event.ctrlKey) { + dom.EventHelper.stop(event, true); + this.getUI().list.toggleHover(); + } + break; + } + })); + + this.ui = { + container, + styleSheet, + leftActionBar, + titleBar, + title, + description1, + description2, + widget, + rightActionBar, + checkAll, + inputContainer, + filterContainer, + inputBox, + visibleCountContainer, + visibleCount, + countContainer, + count, + okContainer, + ok, + message, + customButtonContainer, + customButton, + list, + progressBar, + onDidAccept: this.onDidAcceptEmitter.event, + onDidCustom: this.onDidCustomEmitter.event, + onDidTriggerButton: this.onDidTriggerButtonEmitter.event, + ignoreFocusOut: false, + keyMods: this.keyMods, + show: controller => this.show(controller), + hide: () => this.hide(), + setVisibilities: visibilities => this.setVisibilities(visibilities), + setEnabled: enabled => this.setEnabled(enabled), + setContextKey: contextKey => this.options.setContextKey(contextKey), + linkOpenerDelegate: content => this.options.linkOpenerDelegate(content) + }; + this.updateStyles(); + return this.ui; + } + + pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { + type R = (O extends { canPickMany: true } ? T[] : T) | undefined; + return new Promise((doResolve, reject) => { + let resolve = (result: R) => { + resolve = doResolve; + options.onKeyMods?.(input.keyMods); + doResolve(result); + }; + if (token.isCancellationRequested) { + resolve(undefined); + return; + } + const input = this.createQuickPick(); + let activeItem: T | undefined; + const disposables = [ + input, + input.onDidAccept(() => { + if (input.canSelectMany) { + resolve(input.selectedItems.slice()); + input.hide(); + } else { + const result = input.activeItems[0]; + if (result) { + resolve(result); + input.hide(); + } + } + }), + input.onDidChangeActive(items => { + const focused = items[0]; + if (focused && options.onDidFocus) { + options.onDidFocus(focused); + } + }), + input.onDidChangeSelection(items => { + if (!input.canSelectMany) { + const result = items[0]; + if (result) { + resolve(result); + input.hide(); + } + } + }), + input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({ + ...event, + removeItem: () => { + const index = input.items.indexOf(event.item); + if (index !== -1) { + const items = input.items.slice(); + const removed = items.splice(index, 1); + const activeItems = input.activeItems.filter(activeItem => activeItem !== removed[0]); + const keepScrollPositionBefore = input.keepScrollPosition; + input.keepScrollPosition = true; + input.items = items; + if (activeItems) { + input.activeItems = activeItems; + } + input.keepScrollPosition = keepScrollPositionBefore; + } + } + })), + input.onDidTriggerSeparatorButton(event => options.onDidTriggerSeparatorButton?.(event)), + input.onDidChangeValue(value => { + if (activeItem && !value && (input.activeItems.length !== 1 || input.activeItems[0] !== activeItem)) { + input.activeItems = [activeItem]; + } + }), + token.onCancellationRequested(() => { + input.hide(); + }), + input.onDidHide(() => { + dispose(disposables); + resolve(undefined); + }), + ]; + input.title = options.title; + input.canSelectMany = !!options.canPickMany; + input.placeholder = options.placeHolder; + input.ignoreFocusOut = !!options.ignoreFocusLost; + input.matchOnDescription = !!options.matchOnDescription; + input.matchOnDetail = !!options.matchOnDetail; + input.matchOnLabel = (options.matchOnLabel === undefined) || options.matchOnLabel; // default to true + input.autoFocusOnList = (options.autoFocusOnList === undefined) || options.autoFocusOnList; // default to true + input.quickNavigate = options.quickNavigate; + input.hideInput = !!options.hideInput; + input.contextKey = options.contextKey; + input.busy = true; + Promise.all([picks, options.activeItem]) + .then(([items, _activeItem]) => { + activeItem = _activeItem; + input.busy = false; + input.items = items; + if (input.canSelectMany) { + input.selectedItems = items.filter(item => item.type !== 'separator' && item.picked) as T[]; + } + if (activeItem) { + input.activeItems = [activeItem]; + } + }); + input.show(); + Promise.resolve(picks).then(undefined, err => { + reject(err); + input.hide(); + }); + }); + } + + private setValidationOnInput(input: IInputBox, validationResult: string | { + content: string; + severity: Severity; + } | null | undefined) { + if (validationResult && isString(validationResult)) { + input.severity = Severity.Error; + input.validationMessage = validationResult; + } else if (validationResult && !isString(validationResult)) { + input.severity = validationResult.severity; + input.validationMessage = validationResult.content; + } else { + input.severity = Severity.Ignore; + input.validationMessage = undefined; + } + } + + input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): Promise { + return new Promise((resolve) => { + if (token.isCancellationRequested) { + resolve(undefined); + return; + } + const input = this.createInputBox(); + const validateInput = options.validateInput || (() => >Promise.resolve(undefined)); + const onDidValueChange = Event.debounce(input.onDidChangeValue, (last, cur) => cur, 100); + let validationValue = options.value || ''; + let validation = Promise.resolve(validateInput(validationValue)); + const disposables = [ + input, + onDidValueChange(value => { + if (value !== validationValue) { + validation = Promise.resolve(validateInput(value)); + validationValue = value; + } + validation.then(result => { + if (value === validationValue) { + this.setValidationOnInput(input, result); + } + }); + }), + input.onDidAccept(() => { + const value = input.value; + if (value !== validationValue) { + validation = Promise.resolve(validateInput(value)); + validationValue = value; + } + validation.then(result => { + if (!result || (!isString(result) && result.severity !== Severity.Error)) { + resolve(value); + input.hide(); + } else if (value === validationValue) { + this.setValidationOnInput(input, result); + } + }); + }), + token.onCancellationRequested(() => { + input.hide(); + }), + input.onDidHide(() => { + dispose(disposables); + resolve(undefined); + }), + ]; + + input.title = options.title; + input.value = options.value || ''; + input.valueSelection = options.valueSelection; + input.prompt = options.prompt; + input.placeholder = options.placeHolder; + input.password = !!options.password; + input.ignoreFocusOut = !!options.ignoreFocusLost; + input.show(); + }); + } + + backButton = backButton; + + createQuickPick(): IQuickPick { + const ui = this.getUI(); + return new QuickPick(ui); + } + + createInputBox(): IInputBox { + const ui = this.getUI(); + return new InputBox(ui); + } + + createQuickWidget(): IQuickWidget { + const ui = this.getUI(); + return new QuickWidget(ui); + } + + private show(controller: IQuickInput) { + const ui = this.getUI(); + this.onShowEmitter.fire(); + const oldController = this.controller; + this.controller = controller; + oldController?.didHide(); + + this.setEnabled(true); + ui.leftActionBar.clear(); + ui.title.textContent = ''; + ui.description1.textContent = ''; + ui.description2.textContent = ''; + dom.reset(ui.widget); + ui.rightActionBar.clear(); + ui.checkAll.checked = false; + // ui.inputBox.value = ''; Avoid triggering an event. + ui.inputBox.placeholder = ''; + ui.inputBox.password = false; + ui.inputBox.showDecoration(Severity.Ignore); + ui.visibleCount.setCount(0); + ui.count.setCount(0); + dom.reset(ui.message); + ui.progressBar.stop(); + ui.list.setElements([]); + ui.list.matchOnDescription = false; + ui.list.matchOnDetail = false; + ui.list.matchOnLabel = true; + ui.list.sortByLabel = true; + ui.ignoreFocusOut = false; + ui.inputBox.toggles = undefined; + + const backKeybindingLabel = this.options.backKeybindingLabel(); + backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); + + ui.container.style.display = ''; + this.updateLayout(); + ui.inputBox.setFocus(); + } + + private setVisibilities(visibilities: Visibilities) { + const ui = this.getUI(); + ui.title.style.display = visibilities.title ? '' : 'none'; + ui.description1.style.display = visibilities.description && (visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; + ui.description2.style.display = visibilities.description && !(visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; + ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; + ui.inputContainer.style.display = visibilities.inputBox ? '' : 'none'; + ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; + ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; + ui.countContainer.style.display = visibilities.count ? '' : 'none'; + ui.okContainer.style.display = visibilities.ok ? '' : 'none'; + ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; + ui.message.style.display = visibilities.message ? '' : 'none'; + ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none'; + ui.list.display(!!visibilities.list); + ui.container.classList.toggle('show-checkboxes', !!visibilities.checkBox); + ui.container.classList.toggle('hidden-input', !visibilities.inputBox && !visibilities.description); + this.updateLayout(); // TODO + } + + private setEnabled(enabled: boolean) { + if (enabled !== this.enabled) { + this.enabled = enabled; + for (const item of this.getUI().leftActionBar.viewItems) { + (item as ActionViewItem).action.enabled = enabled; + } + for (const item of this.getUI().rightActionBar.viewItems) { + (item as ActionViewItem).action.enabled = enabled; + } + this.getUI().checkAll.disabled = !enabled; + this.getUI().inputBox.enabled = enabled; + this.getUI().ok.enabled = enabled; + this.getUI().list.enabled = enabled; + } + } + + hide(reason?: QuickInputHideReason) { + const controller = this.controller; + if (!controller) { + return; + } + + const focusChanged = !dom.isAncestor(document.activeElement, this.ui?.container ?? null); + this.controller = null; + this.onHideEmitter.fire(); + this.getUI().container.style.display = 'none'; + if (!focusChanged) { + let currentElement = this.previousFocusElement; + while (currentElement && !currentElement.offsetParent) { + currentElement = currentElement.parentElement ?? undefined; + } + if (currentElement?.offsetParent) { + currentElement.focus(); + this.previousFocusElement = undefined; + } else { + this.options.returnFocus(); + } + } + controller.didHide(reason); + } + + focus() { + if (this.isDisplayed()) { + const ui = this.getUI(); + if (ui.inputBox.enabled) { + ui.inputBox.setFocus(); + } else { + ui.list.domFocus(); + } + } + } + + toggle() { + if (this.isDisplayed() && this.controller instanceof QuickPick && this.controller.canSelectMany) { + this.getUI().list.toggleCheckbox(); + } + } + + navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { + if (this.isDisplayed() && this.getUI().list.isDisplayed()) { + this.getUI().list.focus(next ? QuickInputListFocus.Next : QuickInputListFocus.Previous); + if (quickNavigate && this.controller instanceof QuickPick) { + this.controller.quickNavigate = quickNavigate; + } + } + } + + async accept(keyMods: IKeyMods = { alt: false, ctrlCmd: false }) { + // When accepting the item programmatically, it is important that + // we update `keyMods` either from the provided set or unset it + // because the accept did not happen from mouse or keyboard + // interaction on the list itself + this.keyMods.alt = keyMods.alt; + this.keyMods.ctrlCmd = keyMods.ctrlCmd; + + this.onDidAcceptEmitter.fire(); + } + + async back() { + this.onDidTriggerButtonEmitter.fire(this.backButton); + } + + async cancel() { + this.hide(); + } + + layout(dimension: dom.IDimension, titleBarOffset: number): void { + this.dimension = dimension; + this.titleBarOffset = titleBarOffset; + this.updateLayout(); + } + + private updateLayout() { + if (this.ui && this.isDisplayed()) { + this.ui.container.style.top = `${this.titleBarOffset}px`; + + const style = this.ui.container.style; + const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + style.width = width + 'px'; + style.marginLeft = '-' + (width / 2) + 'px'; + + this.ui.inputBox.layout(); + this.ui.list.layout(this.dimension && this.dimension.height * 0.4); + } + } + + applyStyles(styles: IQuickInputStyles) { + this.styles = styles; + this.updateStyles(); + } + + private updateStyles() { + if (this.ui) { + const { + quickInputTitleBackground, quickInputBackground, quickInputForeground, widgetBorder, widgetShadow, + } = this.styles.widget; + this.ui.titleBar.style.backgroundColor = quickInputTitleBackground ?? ''; + this.ui.container.style.backgroundColor = quickInputBackground ?? ''; + this.ui.container.style.color = quickInputForeground ?? ''; + this.ui.container.style.border = widgetBorder ? `1px solid ${widgetBorder}` : ''; + this.ui.container.style.boxShadow = widgetShadow ? `0 0 8px 2px ${widgetShadow}` : ''; + this.ui.list.style(this.styles.list); + + const content: string[] = []; + if (this.styles.pickerGroup.pickerGroupBorder) { + content.push(`.quick-input-list .quick-input-list-entry { border-top-color: ${this.styles.pickerGroup.pickerGroupBorder}; }`); + } + if (this.styles.pickerGroup.pickerGroupForeground) { + content.push(`.quick-input-list .quick-input-list-separator { color: ${this.styles.pickerGroup.pickerGroupForeground}; }`); + } + if (this.styles.pickerGroup.pickerGroupForeground) { + content.push(`.quick-input-list .quick-input-list-separator-as-item { color: ${this.styles.pickerGroup.pickerGroupForeground}; }`); + } + + if (this.styles.keybindingLabel.keybindingLabelBackground || + this.styles.keybindingLabel.keybindingLabelBorder || + this.styles.keybindingLabel.keybindingLabelBottomBorder || + this.styles.keybindingLabel.keybindingLabelShadow || + this.styles.keybindingLabel.keybindingLabelForeground) { + content.push('.quick-input-list .monaco-keybinding > .monaco-keybinding-key {'); + if (this.styles.keybindingLabel.keybindingLabelBackground) { + content.push(`background-color: ${this.styles.keybindingLabel.keybindingLabelBackground};`); + } + if (this.styles.keybindingLabel.keybindingLabelBorder) { + // Order matters here. `border-color` must come before `border-bottom-color`. + content.push(`border-color: ${this.styles.keybindingLabel.keybindingLabelBorder};`); + } + if (this.styles.keybindingLabel.keybindingLabelBottomBorder) { + content.push(`border-bottom-color: ${this.styles.keybindingLabel.keybindingLabelBottomBorder};`); + } + if (this.styles.keybindingLabel.keybindingLabelShadow) { + content.push(`box-shadow: inset 0 -1px 0 ${this.styles.keybindingLabel.keybindingLabelShadow};`); + } + if (this.styles.keybindingLabel.keybindingLabelForeground) { + content.push(`color: ${this.styles.keybindingLabel.keybindingLabelForeground};`); + } + content.push('}'); + } + + const newStyles = content.join('\n'); + if (newStyles !== this.ui.styleSheet.textContent) { + this.ui.styleSheet.textContent = newStyles; + } + } + } + + private isDisplayed() { + return this.ui && this.ui.container.style.display !== 'none'; + } +} +export interface IQuickInputControllerHost extends ILayoutService { } + diff --git a/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index e2f1fd70fdb..f797cd31f05 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/src/vs/platform/quickinput/browser/quickInputService.ts @@ -14,12 +14,12 @@ import { IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/l import { IOpenerService } from 'vs/platform/opener/common/opener'; import { QuickAccessController } from 'vs/platform/quickinput/browser/quickAccess'; import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; -import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { defaultButtonStyles, defaultCountBadgeStyles, defaultInputBoxStyles, defaultKeybindingLabelStyles, defaultProgressBarStyles, defaultToggleStyles, getListStyles } from 'vs/platform/theme/browser/defaultStyles'; import { activeContrastBorder, asCssVariable, pickerGroupBorder, pickerGroupForeground, quickInputBackground, quickInputForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, quickInputTitleBackground, widgetBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { QuickInputController, IQuickInputControllerHost, IQuickInputOptions, IQuickInputStyles } from './quickInput'; - +import { IQuickInputOptions, IQuickInputStyles } from './quickInput'; +import { QuickInputController, IQuickInputControllerHost } from 'vs/platform/quickinput/browser/quickInputController'; export class QuickInputService extends Themable implements IQuickInputService { @@ -162,6 +162,10 @@ export class QuickInputService extends Themable implements IQuickInputService { return this.controller.createInputBox(); } + createQuickWidget(): IQuickWidget { + return this.controller.createQuickWidget(); + } + focus() { this.controller.focus(); } diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index e5c5dee9173..47dc660daca 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -125,6 +125,19 @@ export interface IQuickAccessProviderHelp { * The command to bring up this quick access provider. */ readonly commandId?: string; + + /** + * The order of help entries in the Command Center. + * Lower values will be placed above higher values. + * No value will hide this help entry from the Command Center. + */ + readonly commandCenterOrder?: number; + + /** + * An optional label to use for the Command Center entry. If not set + * the description will be used instead. + */ + readonly commandCenterLabel?: string; } export interface IQuickAccessProviderDescriptor { diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 8106d170870..4bec55d912d 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -214,7 +214,8 @@ export interface IQuickInput extends IDisposable { description: string | undefined; /** - * Should be an HTMLElement (TODO: move this entire file into browser) + * Should be an HTMLElement. + * @deprecated Use an IQuickWidget instead */ widget: any | undefined; @@ -222,6 +223,10 @@ export interface IQuickInput extends IDisposable { totalSteps: number | undefined; + buttons: ReadonlyArray; + + readonly onDidTriggerButton: Event; + enabled: boolean; contextKey: string | undefined; @@ -233,6 +238,15 @@ export interface IQuickInput extends IDisposable { show(): void; hide(): void; + + didHide(reason?: QuickInputHideReason): void; +} + +export interface IQuickWidget extends IQuickInput { + /** + * Should be an HTMLElement (TODO: move this entire file into browser) + */ + widget: any | undefined; } export interface IQuickPickWillAcceptEvent { @@ -297,10 +311,6 @@ export interface IQuickPick extends IQuickInput { customHover: string | undefined; - buttons: ReadonlyArray; - - readonly onDidTriggerButton: Event; - readonly onDidTriggerItemButton: Event>; readonly onDidTriggerSeparatorButton: Event; @@ -413,16 +423,6 @@ export interface IInputBox extends IQuickInput { */ readonly onDidAccept: Event; - /** - * Buttons to show in addition to user input submission. - */ - buttons: ReadonlyArray; - - /** - * Event called when a button is selected. - */ - readonly onDidTriggerButton: Event; - /** * Text show below the input box. */ @@ -554,10 +554,15 @@ export interface IQuickInputService { createQuickPick(): IQuickPick; /** - * Provides raw access to the quick input controller. + * Provides raw access to the input box controller. */ createInputBox(): IInputBox; + /** + * Provides raw access to the quick widget controller. + */ + createQuickWidget(): IQuickWidget; + /** * Moves focus into quick input. */ diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 6824b9b1d2c..b50d6523c68 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -13,7 +13,7 @@ import { raceTimeout } from 'vs/base/common/async'; import { unthemedCountStyles } from 'vs/base/browser/ui/countBadge/countBadge'; import { unthemedKeybindingLabelOptions } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { unthemedProgressBarOptions } from 'vs/base/browser/ui/progressbar/progressbar'; -import { QuickInputController } from 'vs/platform/quickinput/browser/quickInput'; +import { QuickInputController } from 'vs/platform/quickinput/browser/quickInputController'; import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ColorScheme } from 'vs/platform/theme/common/theme'; diff --git a/src/vs/platform/telemetry/node/1dsAppender.ts b/src/vs/platform/telemetry/node/1dsAppender.ts index 94f0dc30692..63805143d49 100644 --- a/src/vs/platform/telemetry/node/1dsAppender.ts +++ b/src/vs/platform/telemetry/node/1dsAppender.ts @@ -8,15 +8,24 @@ import { streamToBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRequestOptions } from 'vs/base/parts/request/common/request'; import { IRequestService } from 'vs/platform/request/common/request'; +import * as https from 'https'; import { AbstractOneDataSystemAppender, IAppInsightsCore } from 'vs/platform/telemetry/common/1dsAppender'; +type OnCompleteFunc = (status: number, headers: { [headerName: string]: string }, response?: string) => void; + +interface IResponseData { + headers: { [headerName: string]: string }; + statusCode: number; + responseData: string; +} + /** * Completes a request to submit telemetry to the server utilizing the request service * @param options The options which will be used to make the request * @param requestService The request service * @returns An object containing the headers, statusCode, and responseData */ -async function makeTelemetryRequest(options: IRequestOptions, requestService: IRequestService) { +async function makeTelemetryRequest(options: IRequestOptions, requestService: IRequestService): Promise { const response = await requestService.request(options, CancellationToken.None); const responseData = (await streamToBuffer(response.stream)).toString(); const statusCode = response.res.statusCode ?? 200; @@ -31,30 +40,57 @@ async function makeTelemetryRequest(options: IRequestOptions, requestService: IR /** * Complete a request to submit telemetry to the server utilizing the https module. Only used when the request service is not available * @param options The options which will be used to make the request - * @param httpsModule The https node module * @returns An object containing the headers, statusCode, and responseData */ -function makeLegacyTelemetryRequest(options: IRequestOptions, httpsModule: typeof import('https')) { +async function makeLegacyTelemetryRequest(options: IRequestOptions): Promise { const httpsOptions = { method: options.type, headers: options.headers }; - const req = httpsModule.request(options.url ?? '', httpsOptions, res => { - res.on('data', function (responseData) { - return { - headers: res.headers as Record, - statusCode: res.statusCode ?? 200, - responseData: responseData.toString() - }; + const responsePromise = new Promise((resolve, reject) => { + const req = https.request(options.url ?? '', httpsOptions, res => { + res.on('data', function (responseData) { + resolve({ + headers: res.headers as Record, + statusCode: res.statusCode ?? 200, + responseData: responseData.toString() + }); + }); + // On response with error send status of 0 and a blank response to oncomplete so we can retry events + res.on('error', function (err) { + reject(err); + }); }); - // On response with error send status of 0 and a blank response to oncomplete so we can retry events - res.on('error', function (err) { - throw err; + req.write(options.data, (err) => { + if (err) { + reject(err); + } }); + req.end(); }); - req.write(options.data); - req.end(); - return; + return responsePromise; +} + +async function sendPostAsync(requestService: IRequestService | undefined, payload: IPayloadData, oncomplete: OnCompleteFunc) { + const telemetryRequestData = typeof payload.data === 'string' ? payload.data : new TextDecoder().decode(payload.data); + const requestOptions: IRequestOptions = { + type: 'POST', + headers: { + ...payload.headers, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload.data).toString() + }, + url: payload.urlString, + data: telemetryRequestData + }; + + try { + const responseData = requestService ? await makeTelemetryRequest(requestOptions, requestService) : await makeLegacyTelemetryRequest(requestOptions); + oncomplete(responseData.statusCode, responseData.headers, responseData.responseData); + } catch { + // If it errors out, send status of 0 and a blank response to oncomplete so we can retry events + oncomplete(0, {}); + } } @@ -67,41 +103,11 @@ export class OneDataSystemAppender extends AbstractOneDataSystemAppender { defaultData: { [key: string]: any } | null, iKeyOrClientFactory: string | (() => IAppInsightsCore), // allow factory function for testing ) { - let httpsModule: typeof import('https') | undefined; - if (!requestService) { - httpsModule = require('https'); - } // Override the way events get sent since node doesn't have XHTMLRequest const customHttpXHROverride: IXHROverride = { sendPOST: (payload: IPayloadData, oncomplete) => { - - const telemetryRequestData = typeof payload.data === 'string' ? payload.data : new TextDecoder().decode(payload.data); - const requestOptions: IRequestOptions = { - type: 'POST', - headers: { - ...payload.headers, - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload.data).toString() - }, - url: payload.urlString, - data: telemetryRequestData - }; - - try { - if (requestService) { - makeTelemetryRequest(requestOptions, requestService).then(({ statusCode, headers, responseData }) => { - oncomplete(statusCode, headers, responseData); - }); - } else { - if (!httpsModule) { - throw new Error('https module is undefined'); - } - makeLegacyTelemetryRequest(requestOptions, httpsModule); - } - } catch { - // If it errors out, send status of 0 and a blank response to oncomplete so we can retry events - oncomplete(0, {}); - } + // Fire off the async request without awaiting it + sendPostAsync(requestService, payload, oncomplete); } }; diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 552b1811639..e7b7dcb57bd 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -5,6 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; import { ITerminalOutputMatch, ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; @@ -152,6 +153,7 @@ export interface ICommandDetectionCapability { * the state cannot reliably be detected the fallback of undefined will be used. */ readonly hasInput: boolean | undefined; + readonly currentCommand: ICurrentPartialCommand | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; readonly onCommandExecuted: Event; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index c9005bff55f..f3c2519be9e 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -79,6 +79,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe } return undefined; } + get currentCommand(): ICurrentPartialCommand | undefined { + return this._currentCommand; + } get cwd(): string | undefined { return this._cwd; } private get _isInputting(): boolean { return !!(this._currentCommand.commandStartMarker && !this._currentCommand.commandExecutedMarker); diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index c647edca009..235d8efdbe8 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -121,6 +121,8 @@ export const enum TerminalSettingId { DeveloperPtyHostLatency = 'terminal.integrated.developer.ptyHost.latency', /** Simulated startup delay of the pty host process */ DeveloperPtyHostStartupDelay = 'terminal.integrated.developer.ptyHost.startupDelay', + /** Shows the textarea element */ + DevMode = 'terminal.integrated.developer.devMode' } export const enum PosixShellType { @@ -964,6 +966,7 @@ export interface ITerminalCommandSelector { outputMatcher?: ITerminalOutputMatcher; exitStatus: boolean; commandExitResult: 'success' | 'error'; + kind?: 'fix' | 'explain'; } export interface ITerminalBackend { diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 7e017ab5ccd..31977e91c8c 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -161,9 +161,9 @@ export class PtyService extends Disposable implements IPtyService { resolve(stdout); }); }); - const processesForPort = stdout.split('\n'); + const processesForPort = stdout.split(/\r?\n/).filter(s => !!s.trim()); if (processesForPort.length >= 1) { - const capturePid = /\s+(\d+)\s+/; + const capturePid = /\s+(\d+)(?:\s+|$)/; const processId = processesForPort[0].match(capturePid)?.[1]; if (processId) { try { diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 6328cfd2df9..8a71fe97ce1 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -21,6 +21,7 @@ import './mainThreadLocalization'; import './mainThreadBulkEdits'; import './mainThreadChatProvider'; import './mainThreadChatSlashCommands'; +import './mainThreadChatVariables'; import './mainThreadCodeInsets'; import './mainThreadCLICommands'; import './mainThreadClipboard'; @@ -87,6 +88,8 @@ import './mainThreadSecretState'; import './mainThreadShare'; import './mainThreadProfilContentHandlers'; import './mainThreadSemanticSimilarity'; +import './mainThreadAiRelatedInformation'; +import './mainThreadAiEmbeddingVector'; import './mainThreadIssueReporter'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts b/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts new file mode 100644 index 00000000000..954679e2c52 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; +import { ExtHostAiEmbeddingVectorShape, ExtHostContext, MainContext, MainThreadAiEmbeddingVectorShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IAiEmbeddingVectorProvider, IAiEmbeddingVectorService } from 'vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService'; +import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadAiEmbeddingVector) +export class MainThreadAiEmbeddingVector extends Disposable implements MainThreadAiEmbeddingVectorShape { + private readonly _proxy: ExtHostAiEmbeddingVectorShape; + private readonly _registrations = this._register(new DisposableMap()); + + constructor( + context: IExtHostContext, + @IAiEmbeddingVectorService private readonly _AiEmbeddingVectorService: IAiEmbeddingVectorService, + ) { + super(); + this._proxy = context.getProxy(ExtHostContext.ExtHostAiEmbeddingVector); + } + + $registerAiEmbeddingVectorProvider(model: string, handle: number): void { + const provider: IAiEmbeddingVectorProvider = { + provideAiEmbeddingVector: (strings: string[], token: CancellationToken) => { + return this._proxy.$provideAiEmbeddingVector( + handle, + strings, + token + ); + }, + }; + this._registrations.set(handle, this._AiEmbeddingVectorService.registerAiEmbeddingVectorProvider(model, provider)); + } + + $unregisterAiEmbeddingVectorProvider(handle: number): void { + this._registrations.deleteAndDispose(handle); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadAiRelatedInformation.ts b/src/vs/workbench/api/browser/mainThreadAiRelatedInformation.ts new file mode 100644 index 00000000000..b198f41be47 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadAiRelatedInformation.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; +import { ExtHostAiRelatedInformationShape, ExtHostContext, MainContext, MainThreadAiRelatedInformationShape } from 'vs/workbench/api/common/extHost.protocol'; +import { RelatedInformationType } from 'vs/workbench/api/common/extHostTypes'; +import { IAiRelatedInformationProvider, IAiRelatedInformationService } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; +import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { RelatedInformationResult } from 'vscode'; + +@extHostNamedCustomer(MainContext.MainThreadAiRelatedInformation) +export class MainThreadAiRelatedInformation extends Disposable implements MainThreadAiRelatedInformationShape { + private readonly _proxy: ExtHostAiRelatedInformationShape; + private readonly _registrations = this._register(new DisposableMap()); + + constructor( + context: IExtHostContext, + @IAiRelatedInformationService private readonly _aiRelatedInformationService: IAiRelatedInformationService, + ) { + super(); + this._proxy = context.getProxy(ExtHostContext.ExtHostAiRelatedInformation); + } + + $getAiRelatedInformation(query: string, types: RelatedInformationType[]): Promise { + // TODO: use a real cancellation token + return this._aiRelatedInformationService.getRelatedInformation(query, types, CancellationToken.None); + } + + $registerAiRelatedInformationProvider(handle: number, types: RelatedInformationType[]): void { + const provider: IAiRelatedInformationProvider = { + provideAiRelatedInformation: (query, types, token) => { + return this._proxy.$provideAiRelatedInformation(handle, query, types, token); + }, + }; + this._registrations.set(handle, this._aiRelatedInformationService.registerAiRelatedInformationProvider(types, provider)); + } + + $unregisterAiRelatedInformationProvider(handle: number): void { + this._registrations.deleteAndDispose(handle); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index 4a6634dc248..ddeba6f997b 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -11,6 +11,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostChatShape, ExtHostContext, IChatRequestDto, IChatResponseProgressDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChat, IChatDynamicRequest, IChatProgress, IChatRequest, IChatResponse, IChatResponseProgressFileTreeData, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -100,6 +101,7 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { try { const requestDto: IChatRequestDto = { message: request.message, + variables: request.variables }; const dto = await this._proxy.$provideReply(handle, request.session.id, requestDto, token); return { @@ -140,7 +142,7 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { // Complete an existing deferred promise with resolved content const responsePartId = `${id}_${responsePartHandle}`; const deferredContentPromise = this._activeResponsePartPromises.get(responsePartId); - if (deferredContentPromise && 'treeData' in progress) { + if (deferredContentPromise && isCompleteInteractiveProgressTreeData(progress)) { const withRevivedUris = revive<{ treeData: IChatResponseProgressFileTreeData }>(progress); deferredContentPromise.complete(withRevivedUris); this._activeResponsePartPromises.delete(responsePartId); @@ -151,8 +153,8 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { return; } - // No need to support standalone tree data that's not attached to a placeholder - if ('treeData' in progress) { + // No need to support standalone tree data that's not attached to a placeholder in API + if (isCompleteInteractiveProgressTreeData(progress)) { return; } diff --git a/src/vs/workbench/api/browser/mainThreadChatVariables.ts b/src/vs/workbench/api/browser/mainThreadChatVariables.ts new file mode 100644 index 00000000000..bc7fff86be1 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadChatVariables.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableMap } from 'vs/base/common/lifecycle'; +import { ExtHostChatVariablesShape, ExtHostContext, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IChatVariableData, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadChatVariables) +export class MainThreadChatSlashCommands implements MainThreadChatVariablesShape { + + private readonly _proxy: ExtHostChatVariablesShape; + private readonly _variables = new DisposableMap(); + + constructor( + extHostContext: IExtHostContext, + @IChatVariablesService private readonly _chatVariablesService: IChatVariablesService, + ) { + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatVariables); + } + + dispose(): void { + this._variables.clearAndDisposeAll(); + } + + $registerVariable(handle: number, data: IChatVariableData): void { + const registration = this._chatVariablesService.registerVariable(data, (messageText, token) => { + return this._proxy.$resolveVariable(handle, messageText, token); + }); + this._variables.set(handle, registration); + } + + $unregisterVariable(handle: number): void { + this._variables.deleteAndDispose(handle); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 84247a03e27..fb79323e683 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -931,6 +931,13 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } return provider.resolveDocumentOnDropFileData(requestId, dataId); } + + // --- mapped edits + + $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void { + const provider = new MainThreadMappedEditsProvider(handle, this._proxy, this._uriIdentService); + this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider)); + } } class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider { @@ -1124,3 +1131,17 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages. throw new Error(`Unexpected`); } } + +export class MainThreadMappedEditsProvider implements languages.MappedEditsProvider { + + constructor( + private readonly _handle: number, + private readonly _proxy: ExtHostLanguageFeaturesShape, + private readonly _uriService: IUriIdentityService, + ) { } + + async provideMappedEdits(document: ITextModel, codeBlocks: string[], context: languages.MappedEditsContext, token: CancellationToken): Promise { + const res = await this._proxy.$provideMappedEdits(this._handle, document.uri, codeBlocks, context, token); + return res ? reviveWorkspaceEditDto(res, this._uriService) : null; + } +} diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 4e561ae88bb..00eb17f4982 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -56,7 +56,7 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this._store.dispose(); } - $setEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void { + $setEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void { const kind = this.statusbarService.setOrUpdateEntry(entryId, id, extensionId, name, text, tooltip, command, color, backgroundColor, alignLeft, priority, accessibilityInformation); if (kind === StatusBarUpdateKind.DidDefine) { this._store.add(toDisposable(() => this.statusbarService.unsetEntry(entryId))); diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index daab5df909d..295cafe4de0 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -20,10 +20,9 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { OperatingSystem, OS } from 'vs/base/common/platform'; import { TerminalEditorLocationOptions } from 'vscode'; import { Promises } from 'vs/base/common/async'; -import { TerminalQuickFixType } from 'vs/workbench/api/common/extHostTypes'; import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; import { ITerminalLinkProviderService } from 'vs/workbench/contrib/terminalContrib/links/browser/links'; -import { ITerminalQuickFixService, ITerminalQuickFix } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix'; +import { ITerminalQuickFixService, ITerminalQuickFix, TerminalQuickFixType } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) @@ -448,10 +447,12 @@ export function getOutputMatchForLines(lines: string[], outputMatcher: ITerminal } function parseQuickFix(id: string, source: string, fix: TerminalQuickFix): ITerminalQuickFix { - let type = TerminalQuickFixType.Command; + let type = TerminalQuickFixType.TerminalCommand; if ('uri' in fix) { fix.uri = URI.revive(fix.uri); type = TerminalQuickFixType.Opener; + } else if ('id' in fix) { + type = TerminalQuickFixType.VscodeCommand; } return { id, type, source, ...fix }; } diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index d41e861572b..678285a11c5 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -195,10 +195,10 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun }); } }; - this.tunnelService.setTunnelProvider(tunnelProvider); if (features) { this.tunnelService.setTunnelFeatures(features); } + this.tunnelService.setTunnelProvider(tunnelProvider); // At this point we clearly want the ports view/features since we have a tunnel factory this.contextKeyService.createKey(forwardedPortsViewEnabled.key, true); } diff --git a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts index cd2e439f8f6..99dd3279fe8 100644 --- a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts +++ b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts @@ -9,7 +9,7 @@ import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, IStatusbarEntryAccessor, IStatusbarEntry, StatusbarAlignment, IStatusbarEntryPriority } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, IStatusbarEntryAccessor, IStatusbarEntry, StatusbarAlignment, IStatusbarEntryPriority, StatusbarEntryKind } from 'vs/workbench/services/statusbar/browser/statusbar'; import { ThemeColor } from 'vs/base/common/themables'; import { Command } from 'vs/editor/common/languages'; import { IAccessibilityInformation, isAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; @@ -21,6 +21,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { Iterable } from 'vs/base/common/iterator'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { asStatusBarItemIdentifier } from 'vs/workbench/api/common/extHostTypes'; +import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_BACKGROUND } from 'vs/workbench/common/theme'; // --- service @@ -48,7 +49,7 @@ export interface IExtensionStatusBarItemService { onDidChange: Event; - setOrUpdateEntry(id: string, statusId: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): StatusBarUpdateKind; + setOrUpdateEntry(id: string, statusId: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): StatusBarUpdateKind; unsetEntry(id: string): void; @@ -75,7 +76,7 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { setOrUpdateEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, - command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, + command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined ): StatusBarUpdateKind { // if there are icons in the text use the tooltip for the aria label @@ -91,7 +92,16 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { ariaLabel += `, ${tooltipString}`; } } - const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role }; + let kind: StatusbarEntryKind | undefined = undefined; + switch (backgroundColor?.id) { + case STATUS_BAR_ERROR_ITEM_BACKGROUND: + case STATUS_BAR_WARNING_ITEM_BACKGROUND: + // override well known colors that map to status entry kinds to support associated themable hover colors + kind = backgroundColor.id === STATUS_BAR_ERROR_ITEM_BACKGROUND ? 'error' : 'warning'; + color = undefined; + backgroundColor = undefined; + } + const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role, kind }; if (typeof priority === 'undefined') { priority = 0; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 233f094a801..f9803e1476f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -106,6 +106,9 @@ import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSo import { ExtHostShare } from 'vs/workbench/api/common/extHostShare'; import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider'; import { ExtHostChatSlashCommands } from 'vs/workbench/api/common/extHostChatSlashCommand'; +import { ExtHostChatVariables } from 'vs/workbench/api/common/extHostChatVariables'; +import { ExtHostRelatedInformation } from 'vs/workbench/api/common/extHostAiRelatedInformation'; +import { ExtHostAiEmbeddingVector } from 'vs/workbench/api/common/extHostEmbeddingVector'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -207,8 +210,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInlineChat, new ExtHostInteractiveEditor(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService)); const extHostChatProvider = rpcProtocol.set(ExtHostContext.ExtHostChatProvider, new ExtHostChatProvider(rpcProtocol, extHostLogService)); const extHostChatSlashCommands = rpcProtocol.set(ExtHostContext.ExtHostChatSlashCommands, new ExtHostChatSlashCommands(rpcProtocol, extHostChatProvider, extHostLogService)); + const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol, extHostLogService)); const extHostSemanticSimilarity = rpcProtocol.set(ExtHostContext.ExtHostSemanticSimilarity, new ExtHostSemanticSimilarity(rpcProtocol)); + const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); + const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); const extHostIssueReporter = rpcProtocol.set(ExtHostContext.ExtHostIssueReporter, new ExtHostIssueReporter(rpcProtocol)); const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); @@ -1321,6 +1327,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerSemanticSimilarityProvider(provider: vscode.SemanticSimilarityProvider) { checkProposedApiEnabled(extension, 'semanticSimilarity'); return extHostSemanticSimilarity.registerSemanticSimilarityProvider(extension, provider); + }, + getRelatedInformation(query: string, types: vscode.RelatedInformationType[]): Thenable { + checkProposedApiEnabled(extension, 'aiRelatedInformation'); + return extHostAiRelatedInformation.getRelatedInformation(extension, query, types); + }, + registerRelatedInformationProvider(types: vscode.RelatedInformationType[], provider: vscode.RelatedInformationProvider) { + checkProposedApiEnabled(extension, 'aiRelatedInformation'); + return extHostAiRelatedInformation.registerRelatedInformationProvider(extension, types, provider); + }, + registerEmbeddingVectorProvider(model: string, provider: vscode.EmbeddingVectorProvider) { + checkProposedApiEnabled(extension, 'aiRelatedInformation'); + return extHostAiEmbeddingVector.registerEmbeddingVectorProvider(extension, model, provider); } }; @@ -1337,6 +1355,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I requestChatAccess(id: string) { checkProposedApiEnabled(extension, 'chatRequestAccess'); return extHostChatProvider.requestChatResponseProvider(extension.identifier, id); + }, + registerVariable(name: string, description: string, resolver: vscode.ChatVariableResolver) { + checkProposedApiEnabled(extension, 'chatVariables'); + return extHostChatVariables.registerVariableResolver(extension, name, description, resolver); + }, + registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { + checkProposedApiEnabled(extension, 'mappedEditsProvider'); + return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); } }; @@ -1365,6 +1391,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalOutputAnchor: extHostTypes.TerminalOutputAnchor, ChatMessage: extHostTypes.ChatMessage, ChatMessageRole: extHostTypes.ChatMessageRole, + ChatVariableLevel: extHostTypes.ChatVariableLevel, CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall, CallHierarchyItem: extHostTypes.CallHierarchyItem, CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall, @@ -1471,7 +1498,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalLocation: extHostTypes.TerminalLocation, TerminalProfile: extHostTypes.TerminalProfile, TerminalExitReason: extHostTypes.TerminalExitReason, - TerminalQuickFixType: extHostTypes.TerminalQuickFixType, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, @@ -1554,7 +1580,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, StackFrameFocus: extHostTypes.StackFrameFocus, ThreadFocus: extHostTypes.ThreadFocus, - NotebookCodeActionKind: extHostTypes.NotebookCodeActionKind + NotebookCodeActionKind: extHostTypes.NotebookCodeActionKind, + RelatedInformationType: extHostTypes.RelatedInformationType }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 02864ebf389..93826ca0f5f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -75,6 +75,8 @@ import * as search from 'vs/workbench/services/search/common/search'; import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider'; import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatRequestVariableValue, IChatVariableData } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -370,6 +372,16 @@ export interface IShareableItemDto { selection?: IRange; } +export interface IRelatedContextItemDto { + readonly uri: UriComponents; + readonly range: IRange; +} + +export interface IMappedEditsContextDto { + selections: ISelection[]; + related: IRelatedContextItemDto[]; +} + export interface ISignatureHelpProviderMetadataDto { readonly triggerCharacters: readonly string[]; readonly retriggerCharacters: readonly string[]; @@ -425,6 +437,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; + $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void; } export interface MainThreadLanguagesShape extends IDisposable { @@ -1145,6 +1158,15 @@ export interface ExtHostChatSlashCommandsShape { $executeCommand(handle: number, requestId: number, prompt: string, context: { history: IChatMessage[] }, token: CancellationToken): Promise; } +export interface MainThreadChatVariablesShape extends IDisposable { + $registerVariable(handle: number, data: IChatVariableData): void; + $unregisterVariable(handle: number): void; +} + +export interface ExtHostChatVariablesShape { + $resolveVariable(handle: number, messageText: string, token: CancellationToken): Promise; +} + export interface MainThreadInlineChatShape extends IDisposable { $registerInteractiveEditorProvider(handle: number, label: string, debugName: string, supportsFeedback: boolean): Promise; $handleProgressChunk(requestId: string, chunk: { message?: string; edits?: languages.TextEdit[] }): Promise; @@ -1177,6 +1199,7 @@ export interface IChatDto { export interface IChatRequestDto { message: string | IChatReplyFollowup; + variables?: Record; } export interface IChatResponseDto { @@ -1651,6 +1674,25 @@ export interface MainThreadSemanticSimilarityShape extends IDisposable { $unregisterSemanticSimilarityProvider(handle: number): void; } +export interface ExtHostAiRelatedInformationShape { + $provideAiRelatedInformation(handle: number, query: string, types: RelatedInformationType[], token: CancellationToken): Promise; +} + +export interface MainThreadAiRelatedInformationShape { + $getAiRelatedInformation(query: string, types: RelatedInformationType[]): Promise; + $registerAiRelatedInformationProvider(handle: number, types: RelatedInformationType[]): void; + $unregisterAiRelatedInformationProvider(handle: number): void; +} + +export interface ExtHostAiEmbeddingVectorShape { + $provideAiEmbeddingVector(handle: number, strings: string[], token: CancellationToken): Promise; +} + +export interface MainThreadAiEmbeddingVectorShape { + $registerAiEmbeddingVectorProvider(model: string, handle: number): void; + $unregisterAiEmbeddingVectorProvider(handle: number): void; +} + export interface ExtHostSecretStateShape { $onDidChangePassword(e: { extensionId: string; key: string }): Promise; } @@ -1988,6 +2030,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseTypeHierarchy(handle: number, sessionId: string): void; $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; + $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; } export interface ExtHostQuickOpenShape { @@ -2567,6 +2610,7 @@ export const MainContext = { MainThreadBulkEdits: createProxyIdentifier('MainThreadBulkEdits'), MainThreadChatProvider: createProxyIdentifier('MainThreadChatProvider'), MainThreadChatSlashCommands: createProxyIdentifier('MainThreadChatSlashCommands'), + MainThreadChatVariables: createProxyIdentifier('MainThreadChatVariables'), MainThreadClipboard: createProxyIdentifier('MainThreadClipboard'), MainThreadCommands: createProxyIdentifier('MainThreadCommands'), MainThreadComments: createProxyIdentifier('MainThreadComments'), @@ -2629,6 +2673,8 @@ export const MainContext = { MainThreadTesting: createProxyIdentifier('MainThreadTesting'), MainThreadLocalization: createProxyIdentifier('MainThreadLocalizationShape'), MainThreadSemanticSimilarity: createProxyIdentifier('MainThreadSemanticSimilarity'), + MainThreadAiRelatedInformation: createProxyIdentifier('MainThreadAiRelatedInformation'), + MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), MainThreadIssueReporter: createProxyIdentifier('MainThreadIssueReporter'), }; @@ -2686,8 +2732,11 @@ export const ExtHostContext = { ExtHostInlineChat: createProxyIdentifier('ExtHostInlineChatShape'), ExtHostChat: createProxyIdentifier('ExtHostChat'), ExtHostChatSlashCommands: createProxyIdentifier('ExtHostChatSlashCommands'), + ExtHostChatVariables: createProxyIdentifier('ExtHostChatVariables'), ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), ExtHostSemanticSimilarity: createProxyIdentifier('ExtHostSemanticSimilarity'), + ExtHostAiRelatedInformation: createProxyIdentifier('ExtHostAiRelatedInformation'), + ExtHostAiEmbeddingVector: createProxyIdentifier('ExtHostAiEmbeddingVector'), ExtHostTheming: createProxyIdentifier('ExtHostTheming'), ExtHostTunnelService: createProxyIdentifier('ExtHostTunnelService'), ExtHostManagedSockets: createProxyIdentifier('ExtHostManagedSockets'), diff --git a/src/vs/workbench/api/common/extHostAiRelatedInformation.ts b/src/vs/workbench/api/common/extHostAiRelatedInformation.ts new file mode 100644 index 00000000000..9dc39e42ae1 --- /dev/null +++ b/src/vs/workbench/api/common/extHostAiRelatedInformation.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostAiRelatedInformationShape, IMainContext, MainContext, MainThreadAiRelatedInformationShape } from 'vs/workbench/api/common/extHost.protocol'; +import type { CancellationToken, RelatedInformationProvider, RelatedInformationResult, RelatedInformationType } from 'vscode'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; + +export class ExtHostRelatedInformation implements ExtHostAiRelatedInformationShape { + private _relatedInformationProviders: Map = new Map(); + private _nextHandle = 0; + + private readonly _proxy: MainThreadAiRelatedInformationShape; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadAiRelatedInformation); + } + + async $provideAiRelatedInformation(handle: number, query: string, types: RelatedInformationType[], token: CancellationToken): Promise { + if (this._relatedInformationProviders.size === 0) { + throw new Error('No semantic similarity providers registered'); + } + + const provider = this._relatedInformationProviders.get(handle); + if (!provider) { + throw new Error('Semantic similarity provider not found'); + } + + // TODO: should this return undefined or an empty array? + const result = await provider.provideRelatedInformation(query, types, token) ?? []; + return result; + } + + getRelatedInformation(extension: IExtensionDescription, query: string, types: RelatedInformationType[]): Promise { + return this._proxy.$getAiRelatedInformation(query, types); + } + + registerRelatedInformationProvider(extension: IExtensionDescription, types: RelatedInformationType[], provider: RelatedInformationProvider): Disposable { + const handle = this._nextHandle; + this._nextHandle++; + this._relatedInformationProviders.set(handle, provider); + this._proxy.$registerAiRelatedInformationProvider(handle, types); + return new Disposable(() => { + this._proxy.$unregisterAiRelatedInformationProvider(handle); + this._relatedInformationProviders.delete(handle); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 68db008d1b1..84545808c0f 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -464,7 +464,26 @@ const newCommands: ApiCommand[] = [ new ApiCommandArgument('value', 'The context key value', () => true, v => v), ], ApiCommandResult.Void - ) + ), + // --- mapped edits + new ApiCommand( + 'vscode.executeMappedEditsProvider', '_executeMappedEditsProvider', 'Execute Mapped Edits Provider', + [ + ApiCommandArgument.Uri, + ApiCommandArgument.StringArray, + new ApiCommandArgument( + 'MappedEditsContext', + 'Mapped Edits Context', + (v: unknown) => typeConverters.MappedEditsContext.is(v), + (v: vscode.MappedEditsContext) => typeConverters.MappedEditsContext.from(v) + ) + ], + new ApiCommandResult( + 'A promise that resolves to a workspace edit or null', + (value) => { + return value ? typeConverters.WorkspaceEdit.to(value) : null; + }) + ), ]; //#endregion diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index 674b557b889..db4f90727ba 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -200,8 +200,15 @@ export class ExtHostChat implements ExtHostChatShape { const requestObj: vscode.InteractiveRequest = { session: realSession, message: typeof request.message === 'string' ? request.message : typeConvert.ChatReplyFollowup.to(request.message), + variables: {} }; + if (request.variables) { + for (const key of Object.keys(request.variables)) { + requestObj.variables[key] = request.variables[key].map(typeConvert.ChatVariable.to); + } + } + const stopWatch = StopWatch.create(false); let firstProgress: number | undefined; const progressObj: vscode.Progress = { diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts new file mode 100644 index 00000000000..8f142a13a99 --- /dev/null +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostChatVariablesShape, IMainContext, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IChatRequestVariableValue, IChatVariableData } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { ChatVariable } from 'vs/workbench/api/common/extHostTypeConverters'; + +export class ExtHostChatVariables implements ExtHostChatVariablesShape { + + private static _idPool = 0; + + private readonly _resolver = new Map(); + private readonly _proxy: MainThreadChatVariablesShape; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadChatVariables); + } + + async $resolveVariable(handle: number, messageText: string, token: CancellationToken): Promise { + const item = this._resolver.get(handle); + if (!item) { + return undefined; + } + try { + const value = await item.resolver.resolve(item.data.name, { message: messageText }, token); + if (value) { + return value.map(ChatVariable.from); + } + } catch (err) { + onUnexpectedExternalError(err); + } + return undefined; + } + + registerVariableResolver(extension: IExtensionDescription, name: string, description: string, resolver: vscode.ChatVariableResolver): IDisposable { + const handle = ExtHostChatVariables._idPool++; + this._resolver.set(handle, { extension: extension.identifier, data: { name, description }, resolver: resolver }); + this._proxy.$registerVariable(handle, { name, description }); + + return toDisposable(() => { + this._resolver.delete(handle); + this._proxy.$unregisterVariable(handle); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index e8fc48765c7..fd3ba6c52b9 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -444,6 +444,16 @@ export class ApiCommandArgument { static readonly Selection = new ApiCommandArgument('selection', 'A selection in a text document', v => extHostTypes.Selection.isSelection(v), extHostTypeConverter.Selection.from); static readonly Number = new ApiCommandArgument('number', '', v => typeof v === 'number', v => v); static readonly String = new ApiCommandArgument('string', '', v => typeof v === 'string', v => v); + static readonly StringArray = ApiCommandArgument.Arr(ApiCommandArgument.String); + + static Arr(element: ApiCommandArgument) { + return new ApiCommandArgument( + `${element.name}_array`, + `Array of ${element.name}, ${element.description}`, + (v: unknown) => Array.isArray(v) && v.every(e => element.validate(e)), + (v: T[]) => v.map(e => element.convert(e)) + ); + } static readonly CallHierarchyItem = new ApiCommandArgument('item', 'A call hierarchy item', v => v instanceof extHostTypes.CallHierarchyItem, extHostTypeConverter.CallHierarchyItem.from); static readonly TypeHierarchyItem = new ApiCommandArgument('item', 'A type hierarchy item', v => v instanceof extHostTypes.TypeHierarchyItem, extHostTypeConverter.TypeHierarchyItem.from); diff --git a/src/vs/workbench/api/common/extHostEmbeddingVector.ts b/src/vs/workbench/api/common/extHostEmbeddingVector.ts new file mode 100644 index 00000000000..20b3bcb266b --- /dev/null +++ b/src/vs/workbench/api/common/extHostEmbeddingVector.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostAiEmbeddingVectorShape, IMainContext, MainContext, MainThreadAiEmbeddingVectorShape } from 'vs/workbench/api/common/extHost.protocol'; +import type { CancellationToken, EmbeddingVectorProvider } from 'vscode'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; + +export class ExtHostAiEmbeddingVector implements ExtHostAiEmbeddingVectorShape { + private _AiEmbeddingVectorProviders: Map = new Map(); + private _nextHandle = 0; + + private readonly _proxy: MainThreadAiEmbeddingVectorShape; + + constructor( + mainContext: IMainContext + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadAiEmbeddingVector); + } + + async $provideAiEmbeddingVector(handle: number, strings: string[], token: CancellationToken): Promise { + if (this._AiEmbeddingVectorProviders.size === 0) { + throw new Error('No embedding vector providers registered'); + } + + const provider = this._AiEmbeddingVectorProviders.get(handle); + if (!provider) { + throw new Error('Embedding vector provider not found'); + } + + const result = await provider.provideEmbeddingVector(strings, token); + if (!result) { + throw new Error('Embedding vector provider returned undefined'); + } + return result; + } + + registerEmbeddingVectorProvider(extension: IExtensionDescription, model: string, provider: EmbeddingVectorProvider): Disposable { + const handle = this._nextHandle; + this._nextHandle++; + this._AiEmbeddingVectorProviders.set(handle, provider); + this._proxy.$registerAiEmbeddingVectorProvider(model, handle); + return new Disposable(() => { + this._proxy.$unregisterAiEmbeddingVectorProvider(handle); + this._AiEmbeddingVectorProviders.delete(handle); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 34e70b79107..0c35ea254f3 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -532,7 +532,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return that.extensionRuntime; }, get environmentVariableCollection() { return that._extHostTerminalService.getEnvironmentVariableCollection(extensionDescription); }, - getEnvironmentVariableCollection(scope?: vscode.EnvironmentVariableScope) { return that._extHostTerminalService.getEnvironmentVariableCollection(extensionDescription, scope); }, get messagePassingProtocol() { if (!messagePassingProtocol) { if (!messagePort) { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 359877e5509..ec2fbcebeef 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -14,7 +14,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import * as extHostProtocol from './extHost.protocol'; -import { regExpLeadsToEndlessLoop, regExpFlags } from 'vs/base/common/strings'; +import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; import { isFalsyOrEmpty, isNonEmptyArray, coalesce } from 'vs/base/common/arrays'; @@ -1217,7 +1217,6 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { const normalizedResult = Array.isArray(result) ? result : result.items; const commands = this._isAdditionsProposedApiEnabled ? Array.isArray(result) ? [] : result.commands || [] : []; - const suppressSuggestions = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.suppressSuggestions : undefined; const enableForwardStability = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.enableForwardStability : undefined; let disposableStore: DisposableStore | undefined = undefined; @@ -1255,7 +1254,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } return this._commands.toInternal(c, disposableStore); }), - suppressSuggestions, + suppressSuggestions: false, enableForwardStability, }; } @@ -1818,6 +1817,34 @@ class DocumentOnDropEditAdapter { } } +class MappedEditsAdapter { + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.MappedEditsProvider, + ) { } + + async provideMappedEdits( + resource: UriComponents, + codeBlocks: string[], + context: extHostProtocol.IMappedEditsContextDto, + token: CancellationToken + ): Promise { + + const uri = URI.revive(resource); + const doc = this._documents.getDocument(uri); + + const ctx = { + selections: context.selections.map(s => typeConvert.Selection.to(s)), + related: context.related.map(r => ({ uri: URI.revive(r.uri), range: typeConvert.Range.to(r.range) })), + }; + + const mappedEdits = await this._provider.provideMappedEdits(doc, codeBlocks, ctx, token); + + return mappedEdits ? typeConvert.WorkspaceEdit.from(mappedEdits) : null; + } +} + type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter @@ -1827,7 +1854,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter - | DocumentOnDropEditAdapter; + | DocumentOnDropEditAdapter | MappedEditsAdapter; class AdapterData { constructor( @@ -2384,7 +2411,14 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext, token: CancellationToken): Promise { - return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context, token), undefined, token); + return this._withAdapter( + handle, + FoldingProviderAdapter, + (adapter) => + adapter.provideFoldingRanges(URI.revive(resource), context, token), + undefined, + token + ); } // --- smart select @@ -2463,6 +2497,19 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); } + // --- mapped edits + + registerMappedEditsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider): vscode.Disposable { + const handle = this._addNewAdapter(new MappedEditsAdapter(this._documents, provider), extension); + this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension)); + return this._createDisposable(handle); + } + + $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: extHostProtocol.IMappedEditsContextDto, token: CancellationToken): Promise { + return this._withAdapter(handle, MappedEditsAdapter, adapter => + Promise.resolve(adapter.provideMappedEdits(document, codeBlocks, context, token)), null, token); + } + // --- copy/paste actions registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { @@ -2491,7 +2538,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF private static _serializeRegExp(regExp: RegExp): extHostProtocol.IRegExpDto { return { pattern: regExp.source, - flags: regExpFlags(regExp), + flags: regExp.flags, }; } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index d91e8fe990f..c7d8b2a83d0 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -51,8 +51,13 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable; registerProfileProvider(extension: IExtensionDescription, id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable; registerTerminalQuickFixProvider(id: string, extensionId: string, provider: vscode.TerminalQuickFixProvider): vscode.Disposable; - getEnvironmentVariableCollection(extension: IExtensionDescription, scope?: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection; + getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection; } + +interface IEnvironmentVariableCollection extends vscode.EnvironmentVariableCollection { + getScoped(scope: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection; +} + export interface ITerminalInternalOptions { isFeatureTerminal?: boolean; useShellEnvironment?: boolean; @@ -850,13 +855,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return index; } - public getEnvironmentVariableCollection(extension: IExtensionDescription, scope?: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection { + public getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection { let collection = this._environmentVariableCollections.get(extension.identifier.value); if (!collection) { collection = new UnifiedEnvironmentVariableCollection(extension); this._setEnvironmentVariableCollection(extension.identifier.value, collection); } - return collection.getScopedEnvironmentVariableCollection(scope); + return collection.getScopedEnvironmentVariableCollection(undefined); } private _syncEnvironmentVariableCollection(extensionIdentifier: string, collection: UnifiedEnvironmentVariableCollection): void { @@ -923,7 +928,7 @@ class UnifiedEnvironmentVariableCollection { this.map = new Map(serialized); } - getScopedEnvironmentVariableCollection(scope: vscode.EnvironmentVariableScope | undefined): vscode.EnvironmentVariableCollection { + getScopedEnvironmentVariableCollection(scope: vscode.EnvironmentVariableScope | undefined): IEnvironmentVariableCollection { if (this._extension && scope) { // TODO: This should be removed when the env var extension API(s) are stabilized checkProposedApiEnabled(this._extension, 'envCollectionWorkspace'); @@ -942,21 +947,21 @@ class UnifiedEnvironmentVariableCollection { if (this._extension && options) { checkProposedApiEnabled(this._extension, 'envCollectionOptions'); } - this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Replace, options: options ?? {}, scope }); + this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Replace, options: options ?? { applyAtProcessCreation: true }, scope }); } append(variable: string, value: string, options: vscode.EnvironmentVariableMutatorOptions | undefined, scope: vscode.EnvironmentVariableScope | undefined): void { if (this._extension && options) { checkProposedApiEnabled(this._extension, 'envCollectionOptions'); } - this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Append, options: options ?? {}, scope }); + this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Append, options: options ?? { applyAtProcessCreation: true }, scope }); } prepend(variable: string, value: string, options: vscode.EnvironmentVariableMutatorOptions | undefined, scope: vscode.EnvironmentVariableScope | undefined): void { if (this._extension && options) { checkProposedApiEnabled(this._extension, 'envCollectionOptions'); } - this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Prepend, options: options ?? {}, scope }); + this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Prepend, options: options ?? { applyAtProcessCreation: true }, scope }); } private _setIfDiffers(variable: string, mutator: vscode.EnvironmentVariableMutator & { scope: vscode.EnvironmentVariableScope | undefined }): void { @@ -965,22 +970,25 @@ class UnifiedEnvironmentVariableCollection { } const key = this.getKey(variable, mutator.scope); const current = this.map.get(key); + const newOptions = mutator.options ? { + applyAtProcessCreation: mutator.options.applyAtProcessCreation ?? false, + applyAtShellIntegration: mutator.options.applyAtShellIntegration ?? false, + } : { + applyAtProcessCreation: true + }; if ( !current || current.value !== mutator.value || current.type !== mutator.type || - current.options?.applyAtProcessCreation !== (mutator.options.applyAtProcessCreation ?? true) || - current.options?.applyAtShellIntegration !== (mutator.options.applyAtShellIntegration ?? false) || + current.options?.applyAtProcessCreation !== newOptions.applyAtProcessCreation || + current.options?.applyAtShellIntegration !== newOptions.applyAtShellIntegration || current.scope?.workspaceFolder?.index !== mutator.scope?.workspaceFolder?.index ) { const key = this.getKey(variable, mutator.scope); const value: IEnvironmentVariableMutator = { variable, ...mutator, - options: { - applyAtProcessCreation: mutator.options.applyAtProcessCreation ?? true, - applyAtShellIntegration: mutator.options.applyAtShellIntegration ?? false, - } + options: newOptions }; this.map.set(key, value); this._onDidChangeCollection.fire(); @@ -1066,7 +1074,7 @@ class UnifiedEnvironmentVariableCollection { } } -class ScopedEnvironmentVariableCollection implements vscode.EnvironmentVariableCollection { +class ScopedEnvironmentVariableCollection implements IEnvironmentVariableCollection { public get persistent(): boolean { return this.collection.persistent; } public set persistent(value: boolean) { this.collection.persistent = value; @@ -1081,7 +1089,7 @@ class ScopedEnvironmentVariableCollection implements vscode.EnvironmentVariableC ) { } - getScopedEnvironmentVariableCollection(scope: vscode.EnvironmentVariableScope | undefined) { + getScoped(scope: vscode.EnvironmentVariableScope | undefined) { return this.collection.getScopedEnvironmentVariableCollection(scope); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 37868738d64..d5939c95e25 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -45,6 +45,7 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed import type * as vscode from 'vscode'; import * as types from './extHostTypes'; import * as chatProvider from 'vs/workbench/contrib/chat/common/chatProvider'; +import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export namespace Command { @@ -1562,6 +1563,30 @@ export namespace LanguageSelector { } } +export namespace MappedEditsContext { + + export function is(v: unknown): v is vscode.MappedEditsContext { + return (!!v && + typeof v === 'object' && + 'selections' in v && + Array.isArray(v.selections) && + v.selections.every(s => s instanceof types.Selection) && + 'related' in v && + Array.isArray(v.related) && + v.related.every(e => e && typeof e === 'object' && URI.isUri(e.uri) && e.range instanceof types.Range)); + } + + export function from(extContext: vscode.MappedEditsContext): languages.MappedEditsContext { + return { + selections: extContext.selections.map(s => Selection.from(s)), + related: extContext.related.map(r => ({ + uri: URI.from(r.uri), + range: Range.from(r.range) + })) + }; + } +} + export namespace NotebookRange { export function from(range: vscode.NotebookRange): ICellRange { @@ -2151,6 +2176,7 @@ export namespace ChatFollowup { kind: 'command', title: followup.title ?? '', commandId: followup.commandId ?? '', + when: followup.when ?? '', args: followup.args }; } else { @@ -2200,6 +2226,48 @@ export namespace ChatMessageRole { } } +export namespace ChatVariable { + export function to(variable: IChatRequestVariableValue): vscode.ChatVariableValue { + return { + level: ChatVariableLevel.to(variable.level), + value: variable.value, + description: variable.description + }; + } + + export function from(variable: vscode.ChatVariableValue): IChatRequestVariableValue { + return { + level: ChatVariableLevel.from(variable.level), + value: variable.value, + description: variable.description + }; + } +} + +export namespace ChatVariableLevel { + + + export function to(level: 'short' | 'medium' | 'full'): vscode.ChatVariableLevel { + switch (level) { + case 'short': return types.ChatVariableLevel.Short; + case 'medium': return types.ChatVariableLevel.Medium; + case 'full': + default: + return types.ChatVariableLevel.Full; + } + } + export function from(level: vscode.ChatVariableLevel): 'short' | 'medium' | 'full' { + switch (level) { + case types.ChatVariableLevel.Short: return 'short'; + case types.ChatVariableLevel.Medium: return 'medium'; + case types.ChatVariableLevel.Full: + default: + return 'full'; + } + } +} + + export namespace TerminalQuickFix { export function from(quickFix: vscode.TerminalQuickFixExecuteTerminalCommand | vscode.TerminalQuickFixOpener | vscode.Command, converter: Command.ICommandsConverter, disposables: DisposableStore): extHostProtocol.ITerminalQuickFixExecuteTerminalCommandDto | extHostProtocol.ITerminalQuickFixOpenerDto | extHostProtocol.ICommandDto | undefined { if ('terminalCommand' in quickFix) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 723e2e0c500..ac36411c043 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -52,8 +52,9 @@ export enum TerminalOutputAnchor { } export enum TerminalQuickFixType { - Command = 0, - Opener = 1 + TerminalCommand = 0, + Opener = 1, + Command = 3 } @es5ClassCompat @@ -4097,6 +4098,12 @@ export enum ChatMessageRole { Function = 3, } +export enum ChatVariableLevel { + Short = 1, + Medium = 2, + Full = 3 +} + export class ChatMessage implements vscode.ChatMessage { role: ChatMessageRole; @@ -4110,3 +4117,14 @@ export class ChatMessage implements vscode.ChatMessage { } //#endregion + +//#region ai + +export enum RelatedInformationType { + SymbolInformation = 1, + CommandInformation = 2, + SearchInformation = 3, + SettingInformation = 4 +} + +//#endregion diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index a06c4e93d8f..40f1cdaf146 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -97,8 +97,8 @@ export abstract class ErrorHandler { return _prepareStackTrace; }, set(v) { - if (v === prepareStackTraceAndFindExtension || v[_wasWrapped]) { - _prepareStackTrace = v; + if (v === prepareStackTraceAndFindExtension || !v || v[_wasWrapped]) { + _prepareStackTrace = v || prepareStackTraceAndFindExtension; return; } diff --git a/src/vs/workbench/api/test/common/extensionHostMain.test.ts b/src/vs/workbench/api/test/common/extensionHostMain.test.ts index 52b49352d81..e201637150c 100644 --- a/src/vs/workbench/api/test/common/extensionHostMain.test.ts +++ b/src/vs/workbench/api/test/common/extensionHostMain.test.ts @@ -63,6 +63,7 @@ suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slo }] ); + const originalPrepareStackTrace = Error.prepareStackTrace; const insta = new InstantiationService(collection, false); let existingErrorHandler: (e: any) => void; @@ -81,6 +82,10 @@ suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slo findSubstrCount = 0; }); + teardown(() => { + Error.prepareStackTrace = originalPrepareStackTrace; + }); + test('basics', function () { const err = new Error('test1'); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index c196ac2132e..52afb8083d8 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -12,7 +12,7 @@ import { ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree'; import { coalesce } from 'vs/base/common/arrays'; import { UriList, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, markAsSingleton } from 'vs/base/common/lifecycle'; import { stringify } from 'vs/base/common/marshalling'; import { Mimes } from 'vs/base/common/mime'; import { FileAccess, Schemas } from 'vs/base/common/network'; @@ -427,6 +427,7 @@ export class CompositeDragAndDropObserver extends Disposable { static get INSTANCE(): CompositeDragAndDropObserver { if (!CompositeDragAndDropObserver.instance) { CompositeDragAndDropObserver.instance = new CompositeDragAndDropObserver(); + markAsSingleton(CompositeDragAndDropObserver.instance); } return CompositeDragAndDropObserver.instance; @@ -523,7 +524,7 @@ export class CompositeDragAndDropObserver extends Disposable { if (callbacks.onDragEnd) { this.onDragEnd.event(e => { callbacks.onDragEnd!(e); - }); + }, this, disposableStore); } return this._register(disposableStore); diff --git a/src/vs/workbench/browser/parts/editor/accessibilityStatus.ts b/src/vs/workbench/browser/parts/editor/accessibilityStatus.ts index de65eca21f0..5160d1e2791 100644 --- a/src/vs/workbench/browser/parts/editor/accessibilityStatus.ts +++ b/src/vs/workbench/browser/parts/editor/accessibilityStatus.ts @@ -13,8 +13,6 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { INotificationHandle, INotificationService, NotificationPriority } from 'vs/platform/notification/common/notification'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; export class AccessibilityStatus extends Disposable implements IWorkbenchContribution { private screenReaderNotification: INotificationHandle | null = null; @@ -71,8 +69,7 @@ export class AccessibilityStatus extends Disposable implements IWorkbenchContrib text, ariaLabel: text, command: 'showEditorScreenReaderNotification', - backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), - color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) + kind: 'prominent' }, 'status.editor.screenReaderMode', StatusbarAlignment.RIGHT, 100.6); } } else { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index c80fa632d3e..01b92a4b7e2 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -244,7 +244,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const groupEditorsCountContext = EditorGroupEditorsCountContext.bindTo(this.scopedContextKeyService); const groupLockedContext = ActiveEditorGroupLockedContext.bindTo(this.scopedContextKeyService); - const activeEditorListener = new MutableDisposable(); + const activeEditorListener = this._register(new MutableDisposable()); const observeActiveEditor = () => { activeEditorListener.clear(); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 55d6506abab..ff8a2778c7c 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -90,7 +90,7 @@ export class EditorGroupWatermark extends Disposable { } private registerListeners(): void { - this.lifecycleService.onDidShutdown(() => this.dispose()); + this._register(this.lifecycleService.onDidShutdown(() => this.dispose())); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('workbench.tips.enabled')) { diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index d18e4fc12f7..dcb7c46f57a 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -47,8 +47,6 @@ import { Event } from 'vs/base/common/event'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IMarker, IMarkerService, MarkerSeverity, IMarkerData } from 'vs/platform/markers/common/markers'; -import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { AutomaticLanguageDetectionLikelyWrongClassification, AutomaticLanguageDetectionLikelyWrongId, IAutomaticLanguageDetectionLikelyWrongData, ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; @@ -381,8 +379,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { ariaLabel: text, tooltip: localize('disableTabMode', "Disable Accessibility Mode"), command: 'editor.action.toggleTabFocusMode', - backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), - color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) + kind: 'prominent' }, 'status.editor.tabFocusMode', StatusbarAlignment.RIGHT, 100.7); } } else { @@ -400,8 +397,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { ariaLabel: text, tooltip: localize('disableColumnSelectionMode', "Disable Column Selection Mode"), command: 'editor.action.toggleColumnSelection', - backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), - color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) + kind: 'prominent' }, 'status.editor.columnSelectionMode', StatusbarAlignment.RIGHT, 100.8); } } else { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index d712b1a146b..3272b3e4839 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -184,7 +184,7 @@ export class TabsTitleControl extends TitleControl { this.updateTabSizing(false); // Tabs Scrollbar - this.tabsScrollbar = this._register(this.createTabsScrollbar(this.tabsContainer)); + this.tabsScrollbar = this.createTabsScrollbar(this.tabsContainer); this.tabsAndActionsContainer.appendChild(this.tabsScrollbar.getDomNode()); // Tabs Container listeners @@ -206,19 +206,19 @@ export class TabsTitleControl extends TitleControl { } private createTabsScrollbar(scrollable: HTMLElement): ScrollableElement { - const tabsScrollbar = new ScrollableElement(scrollable, { + const tabsScrollbar = this._register(new ScrollableElement(scrollable, { horizontal: ScrollbarVisibility.Auto, horizontalScrollbarSize: this.getTabsScrollbarSizing(), vertical: ScrollbarVisibility.Hidden, scrollYToX: true, useShadows: false - }); + })); - tabsScrollbar.onScroll(e => { + this._register(tabsScrollbar.onScroll(e => { if (e.scrollLeftChanged) { scrollable.scrollLeft = e.scrollLeft; } - }); + })); return tabsScrollbar; } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index b69808128ae..39387591537 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -133,7 +133,7 @@ export class CollapseNotificationAction extends Action { export class ConfigureNotificationAction extends Action { static readonly ID = 'workbench.action.configureNotification'; - static readonly LABEL = localize('configureNotification', "Configure Notification"); + static readonly LABEL = localize('configureNotification', "More Actions..."); constructor( id: string, diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 61880da1a6a..d6897bf02d7 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -142,6 +142,7 @@ .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover:not(.disabled) { text-decoration: none; + color: var(--vscode-statusBarItem-hoverForeground); } .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a.disabled { @@ -154,15 +155,6 @@ color: inherit; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item .status-bar-info { - color: var(--vscode-statusBarItem-prominentForeground); - background-color: var(--vscode-statusBarItem-prominentBackground); -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item a.status-bar-info:hover:not(.disabled) { - background-color: var(--vscode-statusBarItem-prominentHoverBackground); -} - .monaco-workbench .part.statusbar > .items-container > .statusbar-item a:active:not(.disabled) { outline: 1px solid var(--vscode-contrastActiveBorder) !important; outline-offset: -1px; @@ -180,3 +172,55 @@ .monaco-workbench:not(.hc-light):not(.hc-black) .part.statusbar > .items-container > .statusbar-item a:hover:not(.disabled) { background-color: var(--vscode-statusBarItem-hoverBackground); } + +/** Status bar entry item kinds */ + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.warning-kind { + color: var(--vscode-statusBarItem-warningForeground); + background-color: var(--vscode-statusBarItem-warningBackground); +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.warning-kind a:hover:not(.disabled) { + color: var(--vscode-statusBarItem-warningHoverForeground); + background-color: var(--vscode-statusBarItem-warningHoverBackground) !important; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.error-kind { + color: var(--vscode-statusBarItem-errorForeground); + background-color: var(--vscode-statusBarItem-errorBackground); +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.error-kind a:hover:not(.disabled) { + color: var(--vscode-statusBarItem-errorHoverForeground); + background-color: var(--vscode-statusBarItem-errorHoverBackground) !important; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.prominent-kind { + color: var(--vscode-statusBarItem-prominentForeground); + background-color: var(--vscode-statusBarItem-prominentBackground); +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.prominent-kind a:hover:not(.disabled) { + color: var(--vscode-statusBarItem-prominentHoverForeground); + background-color: var(--vscode-statusBarItem-prominentHoverBackground) !important; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.remote-kind { + color: var(--vscode-statusBarItem-remoteForeground); + background-color: var(--vscode-statusBarItem-remoteBackground); +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.remote-kind a:hover:not(.disabled) { + color: var(--vscode-statusBarItem-remoteHoverForeground); + background-color: var(--vscode-statusBarItem-remoteHoverBackground) !important; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.offline-kind { + color: var(--vscode-statusBarItem-offlineForeground); + background-color: var(--vscode-statusBarItem-offlineBackground); +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.offline-kind a:hover:not(.disabled) { + color: var(--vscode-statusBarItem-offlineHoverForeground); + background-color: var(--vscode-statusBarItem-offlineHoverBackground) !important; +} diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index b1988505d95..39fd753e3d6 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -8,7 +8,7 @@ import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IStatusbarEntry, ShowTooltipCommand } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IStatusbarEntry, ShowTooltipCommand, StatusbarEntryKinds } from 'vs/workbench/services/statusbar/browser/statusbar'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ThemeColor } from 'vs/base/common/themables'; @@ -156,6 +156,21 @@ export class StatusbarEntryItem extends Disposable { } } + const hasBackgroundColor = !!entry.backgroundColor || (entry.kind && entry.kind !== 'standard'); + + // Update: Kind + if (!this.entry || entry.kind !== this.entry.kind) { + for (const kind of StatusbarEntryKinds) { + this.container.classList.remove(`${kind}-kind`); + } + + if (entry.kind && entry.kind !== 'standard') { + this.container.classList.add(`${entry.kind}-kind`); + } + + this.container.classList.toggle('has-background-color', hasBackgroundColor); + } + // Update: Foreground if (!this.entry || entry.color !== this.entry.color) { this.applyColor(this.labelContainer, entry.color); @@ -163,7 +178,7 @@ export class StatusbarEntryItem extends Disposable { // Update: Background if (!this.entry || entry.backgroundColor !== this.entry.backgroundColor) { - this.container.classList.toggle('has-background-color', !!entry.backgroundColor); + this.container.classList.toggle('has-background-color', hasBackgroundColor); this.applyColor(this.container, entry.backgroundColor, true); } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 1c949d44a00..a2938f04e10 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -20,7 +20,7 @@ import { EventHelper, createStyleSheet, addDisposableListener, EventType, clearN import { IStorageService } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { coalesce, equals } from 'vs/base/common/arrays'; +import { equals } from 'vs/base/common/arrays'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ToggleStatusbarVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; import { assertIsDefined } from 'vs/base/common/types'; @@ -176,7 +176,7 @@ export class StatusbarPart extends Part implements IStatusbarService { private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { // View model item - const itemContainer = this.doCreateStatusItem(id, alignment, ...coalesce([entry.showBeak ? 'has-beak' : undefined])); + const itemContainer = this.doCreateStatusItem(id, alignment); const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry, this.hoverDelegate); // View model entry diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 4b5269d456e..28ae24ba4b8 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -24,6 +24,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtualWorkspace'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IViewsService } from 'vs/workbench/common/views'; const enum WindowSettingNames { titleSeparator = 'window.titleSeparator', @@ -53,7 +54,8 @@ export class WindowTitle extends Disposable { @IInstantiationService protected readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IViewsService private readonly viewsService: IViewsService ) { super(); this.registerListeners(); @@ -75,6 +77,7 @@ export class WindowTitle extends Disposable { this._register(this.contextService.onDidChangeWorkspaceName(() => this.titleUpdater.schedule())); this._register(this.labelService.onDidChangeFormatters(() => this.titleUpdater.schedule())); this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.titleUpdater.schedule())); + this._register(this.viewsService.onDidChangeFocusedView(() => this.titleUpdater.schedule())); } private onConfigurationChanged(event: IConfigurationChangeEvent): void { @@ -237,6 +240,7 @@ export class WindowTitle extends Disposable { const profileName = this.userDataProfileService.currentProfile.isDefault ? '' : this.userDataProfileService.currentProfile.name; const separator = this.configurationService.getValue(WindowSettingNames.titleSeparator); const titleTemplate = this.configurationService.getValue(WindowSettingNames.title); + const focusedView: string = this.viewsService.getFocusedViewName(); return template(titleTemplate, { activeEditorShort, @@ -254,6 +258,7 @@ export class WindowTitle extends Disposable { appName, remoteName, profileName, + focusedView, separator: { label: separator } }); } diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index e81ea2801ca..ed0b160656b 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -46,6 +46,9 @@ export class ViewsService extends Disposable implements IViewsService { private readonly _onDidChangeViewContainerVisibility = this._register(new Emitter<{ id: string; visible: boolean; location: ViewContainerLocation }>()); readonly onDidChangeViewContainerVisibility = this._onDidChangeViewContainerVisibility.event; + private readonly _onDidChangeFocusedView = this._register(new Emitter()); + readonly onDidChangeFocusedView = this._onDidChangeFocusedView.event; + private readonly visibleViewContextKeys: Map>; private readonly focusedViewContextKey: IContextKey; @@ -235,6 +238,11 @@ export class ViewsService extends Disposable implements IViewsService { return null; } + getFocusedViewName(): string { + const viewId: string = this.contextKeyService.getContextKeyValue(FocusedViewContext.key) ?? ''; + return this.viewDescriptorService.getViewDescriptorById(viewId.toString())?.name ?? ''; + } + async openView(id: string, focus?: boolean): Promise { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(id); if (!viewContainer) { @@ -616,10 +624,16 @@ export class ViewsService extends Disposable implements IViewsService { disposables.add(viewPaneContainer.onDidAddViews(views => this.onViewsAdded(views))); disposables.add(viewPaneContainer.onDidChangeViewVisibility(view => this.onViewsVisibilityChanged(view, view.isBodyVisible()))); disposables.add(viewPaneContainer.onDidRemoveViews(views => this.onViewsRemoved(views))); - disposables.add(viewPaneContainer.onDidFocusView(view => this.focusedViewContextKey.set(view.id))); + disposables.add(viewPaneContainer.onDidFocusView(view => { + if (this.focusedViewContextKey.get() !== view.id) { + this.focusedViewContextKey.set(view.id); + this._onDidChangeFocusedView.fire(); + } + })); disposables.add(viewPaneContainer.onDidBlurView(view => { if (this.focusedViewContextKey.get() === view.id) { this.focusedViewContextKey.reset(); + this._onDidChangeFocusedView.fire(); } })); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 50914bd2182..da60053abeb 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -550,6 +550,7 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('appName', "`${appName}`: e.g. VS Code."), localize('remoteName', "`${remoteName}`: e.g. SSH"), localize('dirty', "`${dirty}`: an indicator for when the active editor has unsaved changes."), + localize('focusedView', "`${focusedView}`: the name of the view that is currently focused."), localize('separator', "`${separator}`: a conditional separator (\" - \") that only shows when surrounded by variables with values or static text.") ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 7fc1ae6cbc0..8e3c7b7a811 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -180,6 +180,8 @@ export class EditorGroupModel extends Disposable { private editors: EditorInput[] = []; private mru: EditorInput[] = []; + private readonly editorListeners = new Set(); + private locked = false; private preview: EditorInput | null = null; // editor in preview state @@ -405,6 +407,7 @@ export class EditorGroupModel extends Disposable { private registerEditorListeners(editor: EditorInput): void { const listeners = new DisposableStore(); + this.editorListeners.add(listeners); // Re-emit disposal of editor input as our own event listeners.add(Event.once(editor.onWillDispose)(() => { @@ -453,6 +456,7 @@ export class EditorGroupModel extends Disposable { listeners.add(this.onDidModelChange(event => { if (event.kind === GroupModelChangeKind.EDITOR_CLOSE && event.editor?.matches(editor)) { dispose(listeners); + this.editorListeners.delete(listeners); } })); } @@ -1077,4 +1081,11 @@ export class EditorGroupModel extends Disposable { return this._id; } + + override dispose(): void { + dispose(Array.from(this.editorListeners)); + this.editorListeners.clear(); + + super.dispose(); + } } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 6db644acc7e..a4a46546750 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -496,6 +496,13 @@ export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hov hcLight: Color.black.transparent(0.12) }, localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', { + dark: STATUS_BAR_FOREGROUND, + light: STATUS_BAR_FOREGROUND, + hcDark: STATUS_BAR_FOREGROUND, + hcLight: STATUS_BAR_FOREGROUND +}, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); + export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBarItem.compactHoverBackground', { dark: Color.white.transparent(0.20), light: Color.white.transparent(0.20), @@ -508,21 +515,28 @@ export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem light: STATUS_BAR_FOREGROUND, hcDark: STATUS_BAR_FOREGROUND, hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); +}, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', { dark: Color.black.transparent(0.5), light: Color.black.transparent(0.5), hcDark: Color.black.transparent(0.5), hcLight: Color.black.transparent(0.5), -}, localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); +}, localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); + +export const STATUS_BAR_PROMINENT_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.prominentHoverForeground', { + dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + light: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND +}, localize('statusBarProminentItemHoverForeground', "Status bar prominent items foreground color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.prominentHoverBackground', { dark: Color.black.transparent(0.3), light: Color.black.transparent(0.3), hcDark: Color.black.transparent(0.3), hcLight: null -}, localize('statusBarProminentItemHoverBackground', "Status bar prominent items background color when hovering. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); +}, localize('statusBarProminentItemHoverBackground', "Status bar prominent items background color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ERROR_ITEM_BACKGROUND = registerColor('statusBarItem.errorBackground', { dark: darken(errorForeground, .4), @@ -538,6 +552,20 @@ export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.err hcLight: Color.white }, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.errorHoverForeground', { + dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + light: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND +}, localize('statusBarErrorItemHoverForeground', "Status bar error items foreground color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); + +export const STATUS_BAR_ERROR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.errorHoverBackground', { + dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + light: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND +}, localize('statusBarErrorItemHoverBackground', "Status bar error items background color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); + export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.warningBackground', { dark: darken(editorWarningForeground, .4), light: darken(editorWarningForeground, .4), @@ -552,6 +580,20 @@ export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.w hcLight: Color.white }, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.warningHoverForeground', { + dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + light: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND +}, localize('statusBarWarningItemHoverForeground', "Status bar warning items foreground color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); + +export const STATUS_BAR_WARNING_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.warningHoverBackground', { + dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + light: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND +}, localize('statusBarWarningItemHoverBackground', "Status bar warning items background color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); + // < --- Activity Bar --- > @@ -643,20 +685,62 @@ export const PROFILE_BADGE_FOREGROUND = registerColor('profileBadge.foreground', // < --- Remote --- > -export const STATUS_BAR_HOST_NAME_BACKGROUND = registerColor('statusBarItem.remoteBackground', { +export const STATUS_BAR_REMOTE_ITEM_BACKGROUND = registerColor('statusBarItem.remoteBackground', { dark: ACTIVITY_BAR_BADGE_BACKGROUND, light: ACTIVITY_BAR_BADGE_BACKGROUND, hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, hcLight: ACTIVITY_BAR_BADGE_BACKGROUND }, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); -export const STATUS_BAR_HOST_NAME_FOREGROUND = registerColor('statusBarItem.remoteForeground', { +export const STATUS_BAR_REMOTE_ITEM_FOREGROUND = registerColor('statusBarItem.remoteForeground', { dark: ACTIVITY_BAR_BADGE_FOREGROUND, light: ACTIVITY_BAR_BADGE_FOREGROUND, hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, hcLight: ACTIVITY_BAR_BADGE_FOREGROUND }, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); +export const STATUS_BAR_REMOTE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.remoteHoverForeground', { + dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + light: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND +}, localize('statusBarRemoteItemHoverForeground', "Foreground color for the remote indicator on the status bar when hovering.")); + +export const STATUS_BAR_REMOTE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.remoteHoverBackground', { + dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + light: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcLight: null +}, localize('statusBarRemoteItemHoverBackground', "Background color for the remote indicator on the status bar when hovering.")); + +export const STATUS_BAR_OFFLINE_ITEM_BACKGROUND = registerColor('statusBarItem.offlineBackground', { + dark: '#6c1717', + light: '#6c1717', + hcDark: '#6c1717', + hcLight: '#6c1717' +}, localize('statusBarItemOfflineBackground', "Status bar item background color when the workbench is offline.")); + +export const STATUS_BAR_OFFLINE_ITEM_FOREGROUND = registerColor('statusBarItem.offlineForeground', { + dark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, + light: STATUS_BAR_REMOTE_ITEM_FOREGROUND, + hcDark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, + hcLight: STATUS_BAR_REMOTE_ITEM_FOREGROUND +}, localize('statusBarItemOfflineForeground', "Status bar item foreground color when the workbench is offline.")); + +export const STATUS_BAR_OFFLINE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.offlineHoverForeground', { + dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + light: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, + hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND +}, localize('statusBarOfflineItemHoverForeground', "Status bar item foreground hover color when the workbench is offline.")); + +export const STATUS_BAR_OFFLINE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.offlineHoverBackground', { + dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + light: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, + hcLight: null +}, localize('statusBarOfflineItemHoverBackground', "Status bar item background hover color when the workbench is offline.")); + export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', { dark: ACTIVITY_BAR_BADGE_BACKGROUND, light: ACTIVITY_BAR_BADGE_BACKGROUND, diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 8306ee74803..95f89542805 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -584,9 +584,11 @@ export interface IViewsService { closeViewContainer(id: string): void; getVisibleViewContainer(location: ViewContainerLocation): ViewContainer | null; getActiveViewPaneContainerWithId(viewContainerId: string): IViewPaneContainer | null; + getFocusedViewName(): string; // View APIs readonly onDidChangeViewVisibility: Event<{ id: string; visible: boolean }>; + readonly onDidChangeFocusedView: Event; isViewVisible(id: string): boolean; openView(id: string, focus?: boolean): Promise; closeView(id: string): void; diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index 81deacd41ea..4725fbf0d9d 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -10,7 +10,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { IAccessibleViewService, AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution'; -import { EditorAccessibilityHelpContribution, HoverAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; +import { EditorAccessibilityHelpContribution, HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; registerAccessibilityConfiguration(); registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed); @@ -22,3 +22,4 @@ workbenchRegistry.registerWorkbenchContribution(UnfocusedViewDimmingContribution const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually); +workbenchContributionsRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 516a0f8e840..c8272bc512d 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -4,15 +4,31 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; export const accessibilityHelpIsShown = new RawContextKey('accessibilityHelpIsShown', false, true); export const accessibleViewIsShown = new RawContextKey('accessibleViewIsShown', false, true); +export const accessibleViewSupportsNavigation = new RawContextKey('accessibleViewSupportsNavigation', false, true); +export const accessibleViewVerbosityEnabled = new RawContextKey('accessibleViewVerbosityEnabled', false, true); +export const accessibleViewGoToSymbolSupported = new RawContextKey('accessibleViewGoToSymbolSupported', false, true); +export const accessibleViewCurrentProviderId = new RawContextKey('accessibleViewCurrentProviderId', undefined, undefined); -export const enum AccessibilitySettingId { - UnfocusedViewOpacity = 'accessibility.unfocusedViewOpacity' +/** + * Miscellaneous settings tagged with accessibility and implemented in the accessibility contrib but + * were better to live under workbench for discoverability. + */ +export const enum AccessibilityWorkbenchSettingId { + ViewDimUnfocusedEnabled = 'workbench.view.dimUnfocused.enabled', + ViewDimUnfocusedOpacity = 'workbench.view.dimUnfocused.opacity' +} + +export const enum ViewDimUnfocusedOpacityProperties { + Default = 0.75, + Minimum = 0.2, + Maximum = 1 } export const enum AccessibilityVerbositySettingId { @@ -20,6 +36,7 @@ export const enum AccessibilityVerbositySettingId { DiffEditor = 'accessibility.verbosity.diffEditor', Chat = 'accessibility.verbosity.panelChat', InlineChat = 'accessibility.verbosity.inlineChat', + InlineCompletions = 'accessibility.verbosity.inlineCompletions', KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor', Notebook = 'accessibility.verbosity.notebook', Editor = 'accessibility.verbosity.editor', @@ -55,6 +72,10 @@ const configuration: IConfigurationNode = { description: localize('verbosity.interactiveEditor.description', 'Provide information about how to access the inline editor chat accessibility help menu and alert with hints which describe how to use the feature when the input is focused'), ...baseProperty }, + [AccessibilityVerbositySettingId.InlineCompletions]: { + description: localize('verbosity.inlineCompletions.description', 'Provide information about how to access the inline completions hover and accessible view'), + ...baseProperty + }, [AccessibilityVerbositySettingId.KeybindingsEditor]: { description: localize('verbosity.keybindingsEditor.description', 'Provide information about how to change a keybinding in the keybindings editor when a row is focused'), ...baseProperty @@ -74,19 +95,33 @@ const configuration: IConfigurationNode = { [AccessibilityVerbositySettingId.EditorUntitledHint]: { description: localize('verbosity.editor.untitledhint', 'Provide information about relevant actions in an untitled text editor.'), ...baseProperty - }, - [AccessibilitySettingId.UnfocusedViewOpacity]: { - description: localize('unfocusedViewOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will dim inactive views to make the focused views more obvious.'), - type: 'number', - minimum: 0.2, - maximum: 1, - default: 1, - tags: ['accessibility'] } } }; export function registerAccessibilityConfiguration() { - const configurationRegistry = Registry.as(Extensions.Configuration); - configurationRegistry.registerConfiguration(configuration); + const registry = Registry.as(Extensions.Configuration); + registry.registerConfiguration(configuration); + + registry.registerConfiguration({ + ...workbenchConfigurationNodeBase, + properties: { + [AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled]: { + description: localize('dimUnfocusedEnabled', 'Whether to dim unfocused editors and terminals, making the focused view more obvious.'), + type: 'boolean', + default: false, + tags: ['accessibility'], + scope: ConfigurationScope.MACHINE, + }, + [AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity]: { + description: localize('dimUnfocusedOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will only take effect when {0} is enabled.', `\`#${AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled}#\``), + type: 'number', + minimum: ViewDimUnfocusedOpacityProperties.Minimum, + maximum: ViewDimUnfocusedOpacityProperties.Maximum, + default: ViewDimUnfocusedOpacityProperties.Default, + tags: ['accessibility'], + scope: ConfigurationScope.MACHINE, + } + } + }); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts index 71deb808ff8..80cdebaab8e 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts @@ -35,6 +35,9 @@ import { IAction } from 'vs/base/common/actions'; import { INotificationViewItem } from 'vs/workbench/common/notifications'; import { ThemeIcon } from 'vs/base/common/themables'; import { Codicon } from 'vs/base/common/codicons'; +import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; export class EditorAccessibilityHelpContribution extends Disposable { static ID: 'editorAccessibilityHelpContribution'; @@ -51,7 +54,7 @@ export class EditorAccessibilityHelpContribution extends Disposable { codeEditor = codeEditorService.getActiveCodeEditor()!; } accessibleViewService.show(instantiationService.createInstance(AccessibilityHelpProvider, codeEditor)); - })); + }, EditorContextKeys.focus)); } } @@ -59,7 +62,7 @@ class AccessibilityHelpProvider implements IAccessibleContentProvider { onClose() { this._editor.focus(); } - options: IAccessibleViewOptions = { type: AccessibleViewType.Help, ariaLabel: localize('editor-help', "editor accessibility help"), readMoreUrl: 'https://go.microsoft.com/fwlink/?linkid=851010' }; + options: IAccessibleViewOptions = { type: AccessibleViewType.Help, readMoreUrl: 'https://go.microsoft.com/fwlink/?linkid=851010' }; verbositySettingKey = AccessibilityVerbositySettingId.Editor; constructor( private readonly _editor: ICodeEditor, @@ -104,9 +107,7 @@ class AccessibilityHelpProvider implements IAccessibleContentProvider { export class HoverAccessibleViewContribution extends Disposable { static ID: 'hoverAccessibleViewContribution'; - private _options: IAccessibleViewOptions = { - ariaLabel: localize('hoverAccessibleView', "Hover Accessible View"), language: 'typescript', type: AccessibleViewType.View - }; + private _options: IAccessibleViewOptions = { language: 'typescript', type: AccessibleViewType.View }; constructor() { super(); this._register(AccessibleViewAction.addImplementation(95, 'hover', accessor => { @@ -198,7 +199,7 @@ export class NotificationAccessibleViewContribution extends Disposable { notification.onDidClose(() => accessibleViewService.next()); accessibleViewService.show({ provideContent: () => { - return localize('notification.accessibleView', '{0} Source: {1}', message, notification.source); + return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); }, onClose(): void { focusList(); @@ -222,10 +223,7 @@ export class NotificationAccessibleViewContribution extends Disposable { renderAccessibleView(); }, verbositySettingKey: AccessibilityVerbositySettingId.Notification, - options: { - ariaLabel: localize('notification', "Notification Accessible View"), - type: AccessibleViewType.View - }, + options: { type: AccessibleViewType.View }, actions: getActionsFromNotification(notification) }); return true; @@ -276,3 +274,57 @@ export function alertFocusChange(index: number | undefined, length: number | und return; } +export class InlineCompletionsAccessibleViewContribution extends Disposable { + static ID: 'inlineCompletionsAccessibleViewContribution'; + private _options: IAccessibleViewOptions = { type: AccessibleViewType.View }; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(95, 'inline-completions', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const codeEditorService = accessor.get(ICodeEditorService); + const contextViewService = accessor.get(IContextViewService); + const show = () => { + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!editor) { + return false; + } + const model = InlineCompletionsController.get(editor)?.model.get(); + const state = model?.state.get(); + if (!model || !state) { + return false; + } + const lineText = model.textModel.getLineContent(state.ghostText.lineNumber); + if (!lineText) { + return false; + } + + const ghostText = state.ghostText.renderForScreenReader(lineText); + if (!ghostText) { + return false; + } + this._options.language = editor.getModel()?.getLanguageId() ?? undefined; + accessibleViewService.show({ + verbositySettingKey: AccessibilityVerbositySettingId.InlineCompletions, + provideContent() { return lineText + ghostText; }, + onClose() { + model.stop(); + editor.focus(); + }, + next() { + contextViewService.hideContextView(); + setTimeout(() => model.next().then(() => show()), 50); + }, + previous() { + contextViewService.hideContextView(); + setTimeout(() => model.previous().then(() => show()), 50); + }, + options: this._options + }); + return true; + }; ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); + return show(); + }, + ) + ); + } +} diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 133b9c6615f..68302f41952 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -32,12 +32,13 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IAction } from 'vs/base/common/actions'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; const enum DIMENSIONS { MAX_WIDTH = 600 @@ -53,7 +54,7 @@ export interface IAccessibleContentProvider { actions?: IAction[]; provideContent(): string; onClose(): void; - onKeyDown?(e: IKeyboardEvent): void; + onKeyUp?(e: IKeyboardEvent): void; previous?(): void; next?(): void; /** @@ -85,7 +86,6 @@ export const enum AccessibleViewType { } export interface IAccessibleViewOptions { - ariaLabel: string; readMoreUrl?: string; /** * Defaults to markdown @@ -96,8 +96,14 @@ export interface IAccessibleViewOptions { class AccessibleView extends Disposable { private _editorWidget: CodeEditorWidget; + private _accessiblityHelpIsShown: IContextKey; private _accessibleViewIsShown: IContextKey; + private _accessibleViewSupportsNavigation: IContextKey; + private _accessibleViewVerbosityEnabled: IContextKey; + private _accessibleViewGoToSymbolSupported: IContextKey; + private _accessibleViewCurrentProviderId: IContextKey; + get editorWidget() { return this._editorWidget; } private _editorContainer: HTMLElement; private _currentProvider: IAccessibleContentProvider | undefined; @@ -116,14 +122,20 @@ class AccessibleView extends Disposable { @IMenuService private readonly _menuService: IMenuService ) { super(); + this._accessiblityHelpIsShown = accessibilityHelpIsShown.bindTo(this._contextKeyService); this._accessibleViewIsShown = accessibleViewIsShown.bindTo(this._contextKeyService); + this._accessibleViewSupportsNavigation = accessibleViewSupportsNavigation.bindTo(this._contextKeyService); + this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService); + this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService); + this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService); + this._editorContainer = document.createElement('div'); this._editorContainer.classList.add('accessible-view'); const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID) }; - this._toolbar = this._register(_instantiationService.createInstance(WorkbenchToolBar, this._editorContainer, { ariaLabel: localize('accessibleViewToolbar', "Accessible View Toolbar"), orientation: ActionsOrientation.HORIZONTAL })); + this._toolbar = this._register(_instantiationService.createInstance(WorkbenchToolBar, this._editorContainer, { orientation: ActionsOrientation.HORIZONTAL })); this._toolbar.context = { viewId: 'accessibleView' }; const toolbarElt = this._toolbar.getElement(); toolbarElt.tabIndex = 0; @@ -149,8 +161,12 @@ class AccessibleView extends Disposable { } })); this._register(this._configurationService.onDidChangeConfiguration(e => { - if (this._currentProvider && this._accessiblityHelpIsShown.get() && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) { - this.show(this._currentProvider); + if (this._currentProvider && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) { + if (this._accessiblityHelpIsShown.get()) { + this.show(this._currentProvider); + } + this._accessibleViewVerbosityEnabled.set(this._configurationService.getValue(this._currentProvider.verbositySettingKey)); + this._updateToolbar(this._currentProvider.actions, this._currentProvider.options.type); } })); } @@ -169,6 +185,7 @@ class AccessibleView extends Disposable { onHide: () => { if (!showAccessibleViewHelp) { this._currentProvider = undefined; + this._accessibleViewCurrentProviderId.reset(); } } }; @@ -264,12 +281,21 @@ class AccessibleView extends Disposable { this._accessibleViewIsShown.set(shown); this._accessiblityHelpIsShown.set(!shown); } + if (provider.next && provider.previous) { + this._accessibleViewSupportsNavigation.set(true); + } else { + this._accessibleViewSupportsNavigation.reset(); + } + const verbosityEnabled: boolean = this._configurationService.getValue(provider.verbositySettingKey); + this._accessibleViewVerbosityEnabled.set(verbosityEnabled); + this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false); } private _render(provider: IAccessibleContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { if (!showAccessibleViewHelp) { // don't overwrite the current provider this._currentProvider = provider; + this._accessibleViewCurrentProviderId.set(provider.verbositySettingKey.replaceAll('accessibility.verbosity.', '')); } this._updateContextKeys(provider, true); const value = this._configurationService.getValue(provider.verbositySettingKey); @@ -295,7 +321,7 @@ class AccessibleView extends Disposable { } } - const fragment = message + provider.provideContent() + readMoreLink + disableHelpHint + localize('exit-tip', 'Exit this dialog via the Escape key.'); + const fragment = message + provider.provideContent() + readMoreLink + disableHelpHint + localize('exit-tip', '\nExit this dialog via the Escape key.'); this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment })).then((model) => { if (!model) { @@ -308,35 +334,35 @@ class AccessibleView extends Disposable { } model.setLanguage(provider.options.language ?? 'markdown'); container.appendChild(this._editorContainer); - let ariaLabel = ''; - let helpHint = ''; + let actionsHint = ''; const verbose = this._configurationService.getValue(provider.verbositySettingKey); - if (verbose && provider.options.type === AccessibleViewType.View && !showAccessibleViewHelp) { - const accessibilityHelpKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); - if (accessibilityHelpKeybinding) { - helpHint = localize('ariaAccessibilityHelp', "Use {0} for accessibility help", accessibilityHelpKeybinding); - } + const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || this._currentProvider?.actions; + if (verbose && !showAccessibleViewHelp && hasActions) { + actionsHint = localize('ariaAccessibleViewActions', "Use Shift+Tab to explore actions such as disabling this hint."); } - if (helpHint) { - ariaLabel = provider.options.ariaLabel ? localize('helpAriaKb', "{0}, {1}", provider.options.ariaLabel, helpHint) : localize('accessible-view', "Accessible View, {0}", helpHint); - } else { - ariaLabel = provider.options.ariaLabel ? provider.options.ariaLabel : localize('helpAriaNoKb', "Accessible View"); + let ariaLabel = provider.options.type === AccessibleViewType.Help ? localize('accessibility-help', "Accessibility Help") : localize('accessible-view', "Accessible View"); + if (actionsHint && provider.options.type === AccessibleViewType.View) { + ariaLabel = localize('accessible-view-hint', "Accessible View, {0}", actionsHint); + } else if (actionsHint) { + ariaLabel = localize('accessibility-help-hint', "Accessibility Help, {0}", actionsHint); } - this._editorWidget.updateOptions({ ariaLabel }); this._editorWidget.focus(); }); - this._updateToolbar(provider.actions); + this._updateToolbar(provider.actions, provider.options.type); + const handleEscape = (e: KeyboardEvent | IKeyboardEvent): void => { + e.stopPropagation(); + this._contextViewService.hideContextView(); + this._updateContextKeys(provider, false); + // HACK: Delay to allow the context view to hide #186514 + setTimeout(() => provider.onClose(), 100); + }; const disposableStore = new DisposableStore(); - disposableStore.add(this._editorWidget.onKeyUp((e) => provider.onKeyDown?.(e))); + disposableStore.add(this._editorWidget.onKeyUp((e) => provider.onKeyUp?.(e))); disposableStore.add(this._editorWidget.onKeyDown((e) => { if (e.keyCode === KeyCode.Escape) { - e.stopPropagation(); - this._contextViewService.hideContextView(); - this._updateContextKeys(provider, false); - // HACK: Delay to allow the context view to hide #186514 - setTimeout(() => provider.onClose(), 100); + handleEscape(e); } else if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) { const url: string = provider.options.readMoreUrl!; alert(AccessibilityHelpNLS.openingDocs); @@ -345,6 +371,12 @@ class AccessibleView extends Disposable { e.stopPropagation(); } })); + disposableStore.add(addDisposableListener(this._toolbar.getElement(), EventType.KEY_DOWN, (e: KeyboardEvent) => { + const keyboardEvent = new StandardKeyboardEvent(e); + if (keyboardEvent.equals(KeyCode.Escape)) { + handleEscape(e); + } + })); disposableStore.add(this._editorWidget.onDidBlurEditorWidget(() => { if (document.activeElement !== this._toolbar.getElement()) { this._contextViewService.hideContextView(); @@ -355,8 +387,9 @@ class AccessibleView extends Disposable { return disposableStore; } - private _updateToolbar(providedActions?: IAction[]): void { + private _updateToolbar(providedActions?: IAction[], type?: AccessibleViewType): void { this._toolbar.setActions([]); + this._toolbar.setAriaLabel(type === AccessibleViewType.Help ? localize('accessibleHelpToolbar', 'Accessibility Help') : localize('accessibleViewToolbar', "Accessible View")); const menuActions: IAction[] = []; const toolbarMenu = this._register(this._menuService.createMenu(MenuId.AccessibleView, this._contextKeyService)); createAndFillInActionBarActions(toolbarMenu, {}, menuActions); @@ -387,6 +420,13 @@ class AccessibleView extends Disposable { return this._modelService.createModel(resource.fragment, null, resource, false); } + private _goToSymbolsSupported(): boolean { + if (!this._currentProvider) { + return false; + } + return this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || !!this._currentProvider.getSymbols; + } + public showAccessibleViewHelp(): void { if (!this._currentProvider) { return; @@ -394,14 +434,10 @@ class AccessibleView extends Disposable { } const currentProvider = Object.assign({}, this._currentProvider); currentProvider.options = Object.assign({}, currentProvider.options); - const currentProviderHasSymbols = this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || !!this._currentProvider.getSymbols; const accessibleViewHelpProvider: IAccessibleContentProvider = { - provideContent: () => this._getAccessibleViewHelpDialogContent(currentProviderHasSymbols), + provideContent: () => this._getAccessibleViewHelpDialogContent(this._goToSymbolsSupported()), onClose: () => this.show(currentProvider), - options: { - ariaLabel: localize('accessibleViewHelp', "Accessible View Help"), - type: AccessibleViewType.Help, - }, + options: { type: AccessibleViewType.Help }, verbositySettingKey: this._currentProvider.verbositySettingKey }; this._contextViewService.hideContextView(); @@ -412,7 +448,7 @@ class AccessibleView extends Disposable { private _getAccessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { const navigationHint = this._getNavigationHint(); const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); - const toolbarHint = localize('toolbar', "Navigate to the toolbar ({0} or Shift+Tab)"); + const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))"); let hint = localize('intro', "In the accessible view, you can:\n"); if (navigationHint) { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index da283f254cc..c62bcf7782b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -11,8 +11,11 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { accessibilityHelpIsShown, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { alert } from 'vs/base/browser/ui/aria/aria'; const accessibleViewMenu = { id: MenuId.AccessibleView, @@ -28,12 +31,17 @@ class AccessibleViewNextAction extends Action2 { constructor() { super({ id: AccessibilityCommandId.ShowNext, - precondition: accessibleViewIsShown, + precondition: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), keybinding: { primary: KeyMod.Alt | KeyCode.BracketRight, weight: KeybindingWeight.WorkbenchContrib }, - menu: [commandPalette, accessibleViewMenu], + menu: [ + commandPalette, + { + ...accessibleViewMenu, + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), + }], icon: Codicon.chevronRight, title: localize('editor.action.accessibleViewNext', "Show Next in Accessible View") }); @@ -49,13 +57,19 @@ class AccessibleViewPreviousAction extends Action2 { constructor() { super({ id: AccessibilityCommandId.ShowPrevious, - precondition: accessibleViewIsShown, + precondition: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), keybinding: { primary: KeyMod.Alt | KeyCode.BracketLeft, weight: KeybindingWeight.WorkbenchContrib }, icon: Codicon.chevronLeft, - menu: [commandPalette, accessibleViewMenu], + menu: [ + commandPalette, + { + ...accessibleViewMenu, + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), + } + ], title: localize('editor.action.accessibleViewPrevious', "Show Previous in Accessible View") }); } @@ -70,13 +84,20 @@ class AccessibleViewGoToSymbolAction extends Action2 { constructor() { super({ id: AccessibilityCommandId.GoToSymbol, - precondition: accessibleViewIsShown, + precondition: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewGoToSymbolSupported), keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period], weight: KeybindingWeight.WorkbenchContrib + 10 }, icon: Codicon.symbolField, - menu: [commandPalette, accessibleViewMenu], + menu: [ + commandPalette, + { + ...accessibleViewMenu, + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), + } + ], title: localize('editor.action.accessibleViewGoToSymbol', "Go To Symbol in Accessible View") }); } @@ -134,18 +155,20 @@ class AccessibleViewDisableHintAction extends Action2 { constructor() { super({ id: AccessibilityCommandId.DisableVerbosityHint, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(accessibleViewIsShown, accessibilityHelpIsShown), accessibleViewVerbosityEnabled), keybinding: { - when: ContextKeyExpr.or(accessibleViewIsShown, accessibilityHelpIsShown), primary: KeyMod.Alt | KeyCode.F6, weight: KeybindingWeight.WorkbenchContrib }, icon: Codicon.treeFilterClear, - menu: [commandPalette, + menu: [ + commandPalette, { id: MenuId.AccessibleView, group: 'navigation', - when: ContextKeyExpr.or(accessibleViewIsShown, accessibilityHelpIsShown) - }], + when: ContextKeyExpr.and(ContextKeyExpr.or(accessibleViewIsShown, accessibilityHelpIsShown), accessibleViewVerbosityEnabled), + } + ], title: localize('editor.action.accessibleViewDisableHint', "Disable Accessible View Hint") }); } @@ -154,3 +177,44 @@ class AccessibleViewDisableHintAction extends Action2 { } } registerAction2(AccessibleViewDisableHintAction); + +class AccessibleViewAcceptInlineCompletionAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibleViewAcceptInlineCompletion, + precondition: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibilityVerbositySettingId.InlineCompletions)), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.Slash, + mac: { primary: KeyMod.WinCtrl | KeyCode.Slash }, + weight: KeybindingWeight.WorkbenchContrib + }, + icon: Codicon.check, + menu: [ + commandPalette, + { + id: MenuId.AccessibleView, + group: 'navigation', + order: 0, + when: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibilityVerbositySettingId.InlineCompletions)) + }], + title: localize('editor.action.accessibleViewAcceptInlineCompletionAction', "Accept Inline Completion") + }); + } + async run(accessor: ServicesAccessor): Promise { + const codeEditorService = accessor.get(ICodeEditorService); + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!editor) { + return; + } + const model = InlineCompletionsController.get(editor)?.model.get(); + const state = model?.state.get(); + if (!model || !state) { + return; + } + await model.accept(editor); + alert('Accepted'); + model.stop(); + editor.focus(); + } +} +registerAction2(AccessibleViewAcceptInlineCompletionAction); diff --git a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts index e865074afda..1dc2b4b3605 100644 --- a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts @@ -8,38 +8,35 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { AccessibilitySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityWorkbenchSettingId, ViewDimUnfocusedOpacityProperties } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; export class UnfocusedViewDimmingContribution extends Disposable implements IWorkbenchContribution { + private _styleElement?: HTMLStyleElement; + constructor( @IConfigurationService configurationService: IConfigurationService, ) { super(); - const elStyle = document.createElement('style'); - elStyle.className = 'accessibilityUnfocusedViewOpacity'; - document.head.appendChild(elStyle); - this._register(toDisposable(() => elStyle.remove())); + this._register(toDisposable(() => this._removeStyleElement())); this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => { - if (e && !e.affectsConfiguration(AccessibilitySettingId.UnfocusedViewOpacity)) { + if (e && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled) && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity)) { return; } - let opacity: number; - const opacityConfig = configurationService.getValue(AccessibilitySettingId.UnfocusedViewOpacity); - if (typeof opacityConfig !== 'number') { - opacity = 1; - } else { - opacity = clamp(opacityConfig, 0.2, 1); - } - let cssTextContent = ''; - // Only add the styles if the feature is used - if (opacity !== 1) { - const rules = new Set(); + const enabled = ensureBoolean(configurationService.getValue(AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled), false); + if (enabled) { + const opacity = clamp( + ensureNumber(configurationService.getValue(AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity), ViewDimUnfocusedOpacityProperties.Default), + ViewDimUnfocusedOpacityProperties.Minimum, + ViewDimUnfocusedOpacityProperties.Maximum + ); + if (opacity !== 1) { + const rules = new Set(); const filterRule = `filter: opacity(${opacity});`; // Terminal tabs rules.add(`.monaco-workbench .pane-body.integrated-terminal:not(:focus-within) .tabs-container { ${filterRule} }`); @@ -49,11 +46,39 @@ export class UnfocusedViewDimmingContribution extends Disposable implements IWor rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .monaco-editor { ${filterRule} }`); // Terminal editors rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .terminal-wrapper { ${filterRule} }`); + cssTextContent = [...rules].join('\n'); } - cssTextContent = [...rules].join('\n'); + } - elStyle.textContent = cssTextContent; + if (cssTextContent.length === 0) { + this._removeStyleElement(); + } else { + this._getStyleElement().textContent = cssTextContent; + } })); } + + private _getStyleElement(): HTMLStyleElement { + if (!this._styleElement) { + this._styleElement = document.createElement('style'); + this._styleElement.className = 'accessibilityUnfocusedViewOpacity'; + document.head.appendChild(this._styleElement); + } + return this._styleElement; + } + + private _removeStyleElement(): void { + this._styleElement?.remove(); + this._styleElement = undefined; + } +} + + +function ensureBoolean(value: unknown, defaultValue: boolean): boolean { + return typeof value === 'boolean' ? value : defaultValue; +} + +function ensureNumber(value: unknown, defaultValue: number): number { + return typeof value === 'number' ? value : defaultValue; } diff --git a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts index 8475978456d..2fb8316bdc8 100644 --- a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts +++ b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts @@ -9,5 +9,6 @@ export const enum AccessibilityCommandId { DisableVerbosityHint = 'editor.action.accessibleViewDisableHint', GoToSymbol = 'editor.action.accessibleViewGoToSymbol', ShowNext = 'editor.action.accessibleViewNext', - ShowPrevious = 'editor.action.accessibleViewPrevious' + ShowPrevious = 'editor.action.accessibleViewPrevious', + AccessibleViewAcceptInlineCompletion = 'editor.action.accessibleViewAcceptInlineCompletion' } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index bc2bc2e9430..f8c40b935f7 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -84,6 +84,6 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi InlineChatController.get(editor)?.focus(); } }, - options: { type: AccessibleViewType.Help, ariaLabel: type === 'panelChat' ? localize('chat-help-label', "Chat accessibility help") : localize('inline-chat-label', "Inline chat accessibility help") } + options: { type: AccessibleViewType.Help } }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index e9c9089baf6..3761e137dc5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -30,6 +31,8 @@ import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { WorkspaceEdit, RelatedContextItem } from 'vs/editor/common/languages'; export interface IChatCodeBlockActionContext { code: string; @@ -232,17 +235,44 @@ export function registerChatCodeBlockActions() { this.notifyUserAction(accessor, context); } - private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, context: IChatCodeBlockActionContext) { - this.notifyUserAction(accessor, context); + private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, chatCodeBlockActionContext: IChatCodeBlockActionContext) { + this.notifyUserAction(accessor, chatCodeBlockActionContext); + const bulkEditService = accessor.get(IBulkEditService); const codeEditorService = accessor.get(ICodeEditorService); - const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); - await bulkEditService.apply([new ResourceTextEdit(activeModel.uri, { - range: activeSelection, - text: context.code, - })]); + const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); + // try applying workspace edit that was returned by a MappedEditsProvider, else simply insert at selection + + let workspaceEdit: WorkspaceEdit | null = null; + + if (mappedEditsProviders.length > 0) { + const mostRelevantProvider = mappedEditsProviders[0]; + + const selections = codeEditor.getSelections() ?? []; + const mappedEditsContext = { + selections, + related: [] as RelatedContextItem[], // TODO@ulugbekna: we do have not yet decided what to populate this with + }; + const cancellationTokenSource = new CancellationTokenSource(); + + workspaceEdit = await mostRelevantProvider.provideMappedEdits( + activeModel, + [chatCodeBlockActionContext.code], + mappedEditsContext, + cancellationTokenSource.token); + } + + if (workspaceEdit) { + await bulkEditService.apply(workspaceEdit); + } else { + const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); + await bulkEditService.apply([new ResourceTextEdit(activeModel.uri, { + range: activeSelection, + text: chatCodeBlockActionContext.code, + })]); + } codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 41624ff1316..a2e7aa52b62 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -76,5 +76,5 @@ export function registerChatCopyActions() { function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel): string { return isRequestVM(item) ? - `${item.username}: ${item.messageText}` : `${item.username}: ${item.response.value}`; + `${item.username}: ${item.messageText}` : `${item.username}: ${item.response.asString()}`; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts index bd249edb1cc..07575215554 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts @@ -3,32 +3,69 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from 'vs/base/browser/dom'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -export const ASK_QUICK_QUESTION_ACTION_ID = 'chat.action.askQuickQuestion'; +export const ASK_QUICK_QUESTION_ACTION_ID = 'workbench.action.quickchat.toggle'; export function registerQuickChatActions() { registerAction2(QuickChatGlobalAction); + + registerAction2(class OpenInChatViewAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.quickchat.openInChatView', + title: { + value: localize('chat.openInChatView.label', "Open in Chat View"), + original: 'Open in Chat View' + }, + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.commentDiscussion, + menu: { + id: MenuId.ChatInputSide, + group: 'navigation', + order: 10 + } + }); + } + + run(accessor: ServicesAccessor) { + const quickChatService = accessor.get(IQuickChatService); + quickChatService.openInChatView(); + } + }); + + registerAction2(class CloseQuickChatAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.quickchat.close', + title: { + value: localize('chat.closeQuickChat.label', "Close Quick Chat"), + original: 'Close Quick Chat' + }, + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.close, + menu: { + id: MenuId.ChatInputSide, + group: 'navigation', + order: 20 + } + }); + } + + run(accessor: ServicesAccessor) { + const quickChatService = accessor.get(IQuickChatService); + quickChatService.close(); + } + }); } class QuickChatGlobalAction extends Action2 { @@ -46,24 +83,13 @@ class QuickChatGlobalAction extends Action2 { linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI } - }, - menu: { - id: MenuId.LayoutControlMenu, - group: '0_workbench_toggles', - when: ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'chatView'), - order: 0 } }); } - override async run(accessor: ServicesAccessor, query: string): Promise { - const chatService = accessor.get(IChatService); - const commandService = accessor.get(ICommandService); - // Grab the first provider and run its command - const info = chatService.getProviderInfos()[0]; - if (info) { - await commandService.executeCommand(`workbench.action.openQuickChat.${info.id}`, query); - } + override run(accessor: ServicesAccessor, query?: string): void { + const quickChatService = accessor.get(IQuickChatService); + quickChatService.toggle(undefined, query); } } @@ -77,10 +103,6 @@ class QuickChatGlobalAction extends Action2 { */ export function getQuickChatActionForProvider(id: string, label: string) { return class AskQuickChatAction extends Action2 { - _currentTimer: any | undefined; - _input: IQuickPick | undefined; - _currentChat: QuickChat | undefined; - constructor() { super({ id: `workbench.action.openQuickChat.${id}`, @@ -90,200 +112,9 @@ export function getQuickChatActionForProvider(id: string, label: string) { }); } - override run(accessor: ServicesAccessor, query: string): void { - const quickInputService = accessor.get(IQuickInputService); - const chatService = accessor.get(IChatService); - const instantiationService = accessor.get(IInstantiationService); - - // First things first, clear the existing timer that will dispose the session - clearTimeout(this._currentTimer); - this._currentTimer = undefined; - - // If the input is already shown, hide it. This provides a toggle behavior of the quick pick - if (this._input !== undefined) { - this._input.hide(); - return; - } - - // Check if any providers are available. If not, show nothing - // This shouldn't be needed because of the precondition, but just in case - const providerInfo = chatService.getProviderInfos()[0]; - if (!providerInfo) { - return; - } - - const disposableStore = new DisposableStore(); - - //#region Setup quick pick - - this._input = quickInputService.createQuickPick(); - disposableStore.add(this._input); - this._input.hideInput = true; - - - const containerSession = dom.$('.interactive-session'); - this._input.widget = containerSession; - - this._currentChat ??= instantiationService.createInstance(QuickChat, { - providerId: providerInfo.id, - }); - // show needs to come before the current chat rendering - this._input.show(); - this._currentChat.render(containerSession); - - const clearButton = { - iconClass: ThemeIcon.asClassName(Codicon.clearAll), - tooltip: localize('clear', "Clear"), - }; - this._input.buttons = [ - clearButton, - { - iconClass: ThemeIcon.asClassName(Codicon.commentDiscussion), - tooltip: localize('openInChat', "Open In Chat View"), - } - ]; - this._input.title = providerInfo.displayName; - - disposableStore.add(this._input.onDidHide(() => { - disposableStore.dispose(); - this._input = undefined; - this._currentTimer = setTimeout(() => { - this._currentChat?.dispose(); - this._currentChat = undefined; - }, 1000 * 30); // 30 seconds - })); - - disposableStore.add(this._input.onDidTriggerButton((e) => { - if (e === clearButton) { - this._currentChat?.clear(); - } else { - this._currentChat?.openChatView(); - } - })); - - //#endregion - - this._currentChat.focus(); - - if (query) { - this._currentChat.setValue(query); - this._currentChat.acceptInput(); - } + override run(accessor: ServicesAccessor, query?: string): void { + const quickChatService = accessor.get(IQuickChatService); + quickChatService.toggle(id, query); } }; } - -class QuickChat extends Disposable { - private widget!: ChatWidget; - private model: ChatModel | undefined; - private _currentQuery: string | undefined; - - private _scopedContextKeyService!: IScopedContextKeyService; - get scopedContextKeyService() { - return this._scopedContextKeyService; - } - - constructor( - private readonly _options: IChatViewOptions, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IChatService private readonly chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService - ) { - super(); - } - - clear() { - this.model?.dispose(); - this.model = undefined; - this.updateModel(); - this.widget.inputEditor.setValue(''); - } - - focus(): void { - if (this.widget) { - this.widget.focusInput(); - } - } - - render(parent: HTMLElement): void { - this._scopedContextKeyService?.dispose(); - this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - this.widget?.dispose(); - this.widget = this._register( - scopedInstantiationService.createInstance( - ChatWidget, - { resource: true, renderInputOnTop: true, renderStyle: 'compact' }, - { - listForeground: editorForeground, - listBackground: editorBackground, - inputEditorBackground: inputBackground, - resultEditorBackground: editorBackground - })); - this.widget.render(parent); - this.widget.setVisible(true); - this.widget.setDynamicChatTreeItemLayout(2, 600); - this.updateModel(); - if (this._currentQuery) { - this.widget.inputEditor.setSelection({ - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: this._currentQuery.length + 1 - }); - } - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.widget.inputEditor.onDidChangeModelContent((e) => { - this._currentQuery = this.widget.inputEditor.getValue(); - })); - this._register(this.widget.onDidClear(() => this.clear())); - } - - async acceptInput(): Promise { - return this.widget.acceptInput(); - } - - async openChatView(): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId); - if (!widget?.viewModel || !this.model) { - return; - } - - for (const request of this.model.getRequests()) { - if (request.response?.response.value || request.response?.errorDetails) { - this.chatService.addCompleteRequest(widget.viewModel.sessionId, - request.message as string, - { - message: request.response.response.asString(), - errorDetails: request.response.errorDetails - }); - } else if (request.message) { - - } - } - - const value = this.widget.inputEditor.getValue(); - if (value) { - widget.inputEditor.setValue(value); - } - widget.focusInput(); - } - - setValue(value: string): void { - this.widget.inputEditor.setValue(value); - } - - private updateModel(): void { - this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None); - if (!this.model) { - throw new Error('Could not start chat session'); - } - - this.widget.setModel(this.model, { inputValue: this._currentQuery }); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index bb25bb22939..9e6bf370a27 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -22,7 +22,7 @@ import { registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/ac import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; -import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; @@ -50,7 +50,9 @@ import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/ import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ChatVariablesService, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; +import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -84,18 +86,6 @@ configurationRegistry.registerConfiguration({ type: 'number', description: nls.localize('interactiveSession.editor.lineHeight', "Controls the line height in pixels in chat codeblocks. Use 0 to compute the line height from the font size."), default: 0 - }, - 'chat.experimental.defaultMode': { - type: 'string', - tags: ['experimental'], - enum: ['chatView', 'quickQuestion', 'both'], - enumDescriptions: [ - nls.localize('interactiveSession.defaultMode.chatView', "Use the chat view as the default mode. Displays the chat icon in the Activity Bar."), - nls.localize('interactiveSession.defaultMode.quickQuestion', "Use the quick question as the default mode. Displays the chat icon in the Title Bar."), - nls.localize('interactiveSession.defaultMode.both', "Displays the chat icon in the Activity Bar and the Title Bar which open their respective chat modes.") - ], - description: nls.localize('interactiveSession.defaultMode', "Controls the default mode of the chat experience."), - default: 'chatView' } } }); @@ -211,7 +201,7 @@ class ChatAccessibleViewContribution extends Disposable { alertFocusChange(responseIndex, length, 'previous'); renderAccessibleView(accessibleViewService, widgetService, codeEditorService); }, - options: { ariaLabel: nls.localize('chatAccessibleView', "Chat Accessible View"), type: AccessibleViewType.View } + options: { type: AccessibleViewType.View } }); return true; } @@ -257,7 +247,9 @@ registerClearActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatContributionService, ChatContributionService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); +registerSingleton(IQuickChatService, QuickChatService, InstantiationType.Delayed); registerSingleton(IChatAccessibilityService, ChatAccessibilityService, InstantiationType.Delayed); registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed); registerSingleton(IChatProviderService, ChatProviderService, InstantiationType.Delayed); registerSingleton(IChatSlashCommandService, ChatSlashCommandService, InstantiationType.Delayed); +registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 59e1f4181ef..f0587cfbc84 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const IChatWidgetService = createDecorator('chatWidgetService'); +export const IQuickChatService = createDecorator('quickChatService'); export const IChatAccessibilityService = createDecorator('chatAccessibilityService'); export interface IChatWidgetService { @@ -32,6 +33,14 @@ export interface IChatWidgetService { getWidgetBySessionId(sessionId: string): IChatWidget | undefined; } +export interface IQuickChatService { + readonly _serviceBrand: undefined; + enabled: boolean; + toggle(providerId?: string, query?: string): void; + focus(): void; + close(): void; + openInChatView(): void; +} export interface IChatAccessibilityService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 66759974390..000bc774029 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -126,10 +126,7 @@ export class ChatContributionService implements IChatContributionService { canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ providerId: providerDescriptor.id }]), - when: ContextKeyExpr.and( - ContextKeyExpr.deserialize(providerDescriptor.when), - ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'quickQuestion') - ) + when: ContextKeyExpr.deserialize(providerDescriptor.when) }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer); diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index fe77551dea0..409e4452098 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; const $ = dom.$; @@ -17,6 +18,7 @@ export class ChatFollowups extends Disposable { followups: T[], private readonly options: IButtonStyles | undefined, private readonly clickHandler: (followup: T) => void, + private readonly contextService: IContextKeyService, ) { super(); @@ -25,6 +27,11 @@ export class ChatFollowups extends Disposable { } private renderFollowup(container: HTMLElement, followup: T): void { + + if (followup.kind === 'command' && followup.when && !this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(followup.when))) { + return; + } + const tooltip = 'tooltip' in followup ? followup.tooltip : undefined; const button = this._register(new Button(container, { ...this.options, supportIcons: true, title: tooltip })); if (followup.kind === 'reply') { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 40a6f13a08f..7066089148a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -180,7 +180,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.container = dom.append(container, $('.interactive-input-part')); this.followupsContainer = dom.append(this.container, $('.interactive-input-followups')); - const inputContainer = dom.append(this.container, $('.interactive-input-and-toolbar')); + const inputAndSideToolbar = dom.append(this.container, $('.interactive-input-and-side-toolbar')); + const inputContainer = dom.append(inputAndSideToolbar, $('.interactive-input-and-execute-toolbar')); const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer)); CONTEXT_IN_CHAT_INPUT.bindTo(inputScopedContextKeyService).set(true); @@ -240,6 +241,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge toolbar.getElement().classList.add('interactive-execute-toolbar'); toolbar.context = { widget }; + if (this.options.renderStyle === 'compact') { + const toolbarSide = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputAndSideToolbar, MenuId.ChatInputSide, { + menuOptions: { + shouldForwardArgs: true + } + })); + toolbarSide.getElement().classList.add('chat-side-toolbar'); + toolbarSide.context = { widget }; + } + this.inputModel = this.modelService.getModel(this.inputUri) || this.modelService.createModel('', null, this.inputUri, true); this.inputModel.updateOptions({ bracketColorizationOptions: { enabled: false, independentColorPoolPerBracketType: false } }); this._inputEditor.setModel(this.inputModel); @@ -258,7 +269,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.clearNode(this.followupsContainer); if (items && items.length > 0) { - this.followupsDisposables.add(new ChatFollowups(this.followupsContainer, items, undefined, followup => this._onDidAcceptFollowup.fire(followup))); + this.followupsDisposables.add(new ChatFollowups(this.followupsContainer, items, undefined, followup => this._onDidAcceptFollowup.fire(followup), this.contextKeyService)); } } @@ -280,9 +291,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const editorBorder = 2; const editorPadding = 8; const executeToolbarWidth = 25; + const sideToolbarWidth = this.options.renderStyle === 'compact' ? 20 : 0; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); - this._inputEditor.layout({ width: width - inputPartHorizontalPadding - editorBorder - editorPadding - executeToolbarWidth, height: inputEditorHeight }); + this._inputEditor.layout({ width: width - inputPartHorizontalPadding - editorBorder - editorPadding - executeToolbarWidth - sideToolbarWidth, height: inputEditorHeight }); if (allowRecurse && initialEditorScrollWidth < 10) { // This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index add3f8bafb7..52f971ad1b9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -63,7 +63,7 @@ import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IChatResponseMarkdownRenderData, IChatResponseRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; @@ -103,7 +103,6 @@ export interface IChatListItemRendererOptions { } export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - static readonly cursorCharacter = '\u258c'; static readonly ID = 'item'; private readonly codeBlocksByResponseId = new Map(); @@ -350,7 +349,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickFollowup.fire(followup))); + followup => this._onDidClickFollowup.fire(followup), + templateData.contextKeyService)); } else { const result = this.renderMarkdown(item as IMarkdownString, element, templateData.elementDisposables, templateData); for (const codeElement of result.element.querySelectorAll('code')) { @@ -399,29 +400,92 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + const renderedPart = renderedParts[index]; + // Is this part completely new? + if (!renderedPart) { + if (isInteractiveProgressTreeData(part)) { + partsToRender[index] = part; + } else { + const wordCountResult = this.getDataForProgressiveRender(element, part); + if (wordCountResult !== undefined) { + partsToRender[index] = { + renderedWordCount: wordCountResult.actualWordCount, + lastRenderTime: Date.now(), + isFullyRendered: wordCountResult.isFullString, + }; + wordCountResults[index] = wordCountResult; + } + } + } + + // Did this part go from being a placeholder string to resolved tree data? + else if (isInteractiveProgressTreeData(part) && !isInteractiveProgressTreeData(renderedPart)) { + partsToRender[index] = part; + } + + // Did this part's content change? + else if (!isInteractiveProgressTreeData(part) && !isInteractiveProgressTreeData(renderedPart) && !renderedPart.isFullyRendered) { + const wordCountResult = this.getDataForProgressiveRender(element, part, renderedPart); + if (wordCountResult !== undefined) { + partsToRender[index] = { + renderedWordCount: wordCountResult.actualWordCount, + lastRenderTime: Date.now(), + isFullyRendered: wordCountResult.isFullString, + }; + wordCountResults[index] = wordCountResult; + } + } + }); + + isFullyRendered = partsToRender.length === 0; + if (isFullyRendered && element.isComplete) { // Response is done and content is rendered, so do a normal render this.traceLayout('runProgressiveRender', `end progressive render, index=${index} and clearing renderData, response is complete, index=${index}`); element.renderData = undefined; disposables.clear(); this.basicRenderElement(element.response.value, element, index, templateData); - } else if (renderValue) { - element.renderData = { - renderedWordCount: renderValue.actualWordCount, - lastRenderTime: Date.now(), - isFullyRendered: renderValue.isFullString - }; + } else if (!isFullyRendered) { + let hasRenderedOneMarkdownBlock = false; + partsToRender.forEach((partToRender, index) => { + if (!partToRender) { + return; + } - const plusCursor = (renderValue.value.match(/```\s*$/) ? - renderValue.value + '\n\n' : - renderValue.value) + ` ${ChatListItemRenderer.cursorCharacter}`; - const result = this.renderMarkdown(new MarkdownString(plusCursor), element, disposables, templateData, true); - // Doing the progressive render - dom.clearNode(templateData.value); - templateData.value.appendChild(result.element); - disposables.add(result); + let result; + if (isInteractiveProgressTreeData(partToRender)) { + result = this.renderTreeData(partToRender, element, disposables, templateData, index); + } + + // Avoid doing progressive rendering for multiple markdown parts simultaneously + else if (!hasRenderedOneMarkdownBlock) { + const value = wordCountResults[index].value; + result = this.renderMarkdown(new MarkdownString(value), element, disposables, templateData, true); + hasRenderedOneMarkdownBlock = true; + } + + if (!result) { + return; + } + + // Doing the progressive render + renderedParts[index] = partToRender; + const existingElement = templateData.value.children[index]; + if (existingElement) { + templateData.value.replaceChild(result.element, existingElement); + } else { + templateData.value.appendChild(result.element); + } + disposables.add(result); + }); } else { // Nothing new to render, not done, keep waiting return false; @@ -435,7 +499,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer void } { @@ -558,8 +622,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IChatListItemTemplate): void { @@ -886,28 +949,16 @@ class CodeBlockPart extends Disposable implements IChatResultCodeBlockPart { } private setText(newText: string): void { - let currentText = this.textModel.getLinesContent().join('\n'); + const currentText = this.textModel.getLinesContent().join('\n'); if (newText === currentText) { return; } - let removedChars = 0; - if (currentText.endsWith(` ${ChatListItemRenderer.cursorCharacter}`)) { - removedChars = 2; - } else if (currentText.endsWith(ChatListItemRenderer.cursorCharacter)) { - removedChars = 1; - } - - if (removedChars > 0) { - currentText = currentText.slice(0, currentText.length - removedChars); - } - if (newText.startsWith(currentText)) { const text = newText.slice(currentText.length); const lastLine = this.textModel.getLineCount(); const lastCol = this.textModel.getLineMaxColumn(lastLine); - const insertAtCol = lastCol - removedChars; - this.textModel.applyEdits([{ range: new Range(lastLine, insertAtCol, lastLine, lastCol), text }]); + this.textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]); } else { // console.log(`Failed to optimize setText`); this.textModel.setValue(newText); @@ -999,7 +1050,8 @@ class TreePool extends Disposable { accessibilityProvider: { getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") - } + }, + alwaysConsumeMouseWheel: false }); return tree; @@ -1133,3 +1185,7 @@ class ChatListTreeDataSource implements IAsyncDataSource 0; + } + + get focused(): boolean { + const widget = this._input?.widget as HTMLElement; + if (!widget) { + return false; + } + return dom.isAncestor(document.activeElement, widget); + } + + toggle(providerId?: string, query?: string | undefined): void { + // If the input is already shown, hide it. This provides a toggle behavior of the quick pick + if (this.focused) { + this.close(); + return; + } + + // Check if any providers are available. If not, show nothing + // This shouldn't be needed because of the precondition, but just in case + const providerInfo = providerId + ? this.chatService.getProviderInfos().find(info => info.id === providerId) + : this.chatService.getProviderInfos()[0]; + if (!providerInfo) { + return; + } + + const disposableStore = new DisposableStore(); + + this._input = this.quickInputService.createQuickWidget(); + this._input.contextKey = 'chatInputVisible'; + this._input.ignoreFocusOut = true; + disposableStore.add(this._input); + + const containerSession = dom.$('.interactive-session'); + this._input.widget = containerSession; + + this._currentChat ??= this.instantiationService.createInstance(QuickChat, { + providerId: providerInfo.id, + }); + + // show needs to come before the current chat rendering + this._input.show(); + this._currentChat.render(containerSession); + + disposableStore.add(this._input.onDidHide(() => { + disposableStore.dispose(); + this._input = undefined; + })); + + this._currentChat.focus(); + + if (query) { + this._currentChat.setValue(query); + this._currentChat.acceptInput(); + } + } + focus(): void { + this._currentChat?.focus(); + } + close(): void { + this._input?.dispose(); + } + async openInChatView(): Promise { + await this._currentChat?.openChatView(); + this.close(); + } +} + +class QuickChat extends Disposable { + private widget!: ChatWidget; + private model: ChatModel | undefined; + private _currentQuery: string | undefined; + + private _scopedContextKeyService!: IScopedContextKeyService; + get scopedContextKeyService() { + return this._scopedContextKeyService; + } + + constructor( + private readonly _options: IChatViewOptions, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatService private readonly chatService: IChatService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService + ) { + super(); + } + + clear() { + this.model?.dispose(); + this.model = undefined; + this.updateModel(); + this.widget.inputEditor.setValue(''); + } + + focus(): void { + if (this.widget) { + this.widget.focusInput(); + } + } + + render(parent: HTMLElement): void { + this._scopedContextKeyService?.dispose(); + this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); + const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + this.widget?.dispose(); + this.widget = this._register( + scopedInstantiationService.createInstance( + ChatWidget, + { resource: true, renderInputOnTop: true, renderStyle: 'compact' }, + { + listForeground: quickInputForeground, + listBackground: quickInputBackground, + inputEditorBackground: inputBackground, + resultEditorBackground: quickInputBackground + })); + this.widget.render(parent); + this.widget.setVisible(true); + this.widget.setDynamicChatTreeItemLayout(2, 600); + this.updateModel(); + if (this._currentQuery) { + this.widget.inputEditor.setSelection({ + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: this._currentQuery.length + 1 + }); + } + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.widget.inputEditor.onDidChangeModelContent((e) => { + this._currentQuery = this.widget.inputEditor.getValue(); + })); + this._register(this.widget.onDidClear(() => this.clear())); + } + + async acceptInput(): Promise { + return this.widget.acceptInput(); + } + + async openChatView(): Promise { + const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId); + if (!widget?.viewModel || !this.model) { + return; + } + + for (const request of this.model.getRequests()) { + if (request.response?.response.value || request.response?.errorDetails) { + this.chatService.addCompleteRequest(widget.viewModel.sessionId, + request.message as string, + { + message: request.response.response.value, + errorDetails: request.response.errorDetails, + followups: request.response.followups + }); + } else if (request.message) { + + } + } + + const value = this.widget.inputEditor.getValue(); + if (value) { + widget.inputEditor.setValue(value); + } + widget.focusInput(); + } + + setValue(value: string): void { + this.widget.inputEditor.setValue(value); + } + + private updateModel(): void { + this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None); + if (!this.model) { + throw new Error('Could not start chat session'); + } + + this.widget.setModel(this.model, { inputValue: this._currentQuery }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.css b/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.css index 6f78c0edda9..d2568dbbfed 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.css @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ .chat-slash-command-content-widget { - padding: 1px 3px; - border-radius: 3px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); + border-radius: 3px; + padding: 1px; } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 04cfb24c8f0..859d2bfbaca 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { getWordAtText } from 'vs/editor/common/core/wordHelper'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; @@ -17,17 +17,22 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; +import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + const decorationDescription = 'chat'; const slashCommandPlaceholderDecorationType = 'chat-session-detail'; const slashCommandTextDecorationType = 'chat-session-text'; +const variableTextDecorationType = 'chat-variable-text'; class InputEditorDecorations extends Disposable { @@ -61,6 +66,7 @@ class InputEditorDecorations extends Disposable { } private updateRegisteredDecorationTypes() { + this.codeEditorService.removeDecorationType(variableTextDecorationType); this.codeEditorService.removeDecorationType(slashCommandTextDecorationType); this._slashCommandContentWidget?.hide(); this.codeEditorService.registerDecorationType(decorationDescription, slashCommandTextDecorationType, { @@ -69,6 +75,12 @@ class InputEditorDecorations extends Disposable { contentText: ' ', } }); + const theme = this.themeService.getColorTheme(); + this.codeEditorService.registerDecorationType(decorationDescription, variableTextDecorationType, { + color: theme.getColor(chatSlashCommandForeground)?.toString(), + backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), + borderRadius: '3px' + }); this.updateInputEditorDecorations(); } @@ -162,6 +174,25 @@ class InputEditorDecorations extends Disposable { } else { this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, []); } + + // const variables = this.chatVariablesService.getVariables(); + const variableReg = /(^|\s)@(\w+)(?=(\s|$))/ig; + let match: RegExpMatchArray | null; + const varDecorations: IDecorationOptions[] = []; + while (match = variableReg.exec(inputValue)) { + // const candidate = match[2]; + // if (Iterable.find(variables, v => v.name === candidate)) + varDecorations.push({ + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: match.index! + match[1].length + 1, + endColumn: match.index! + match[0].length + 1 + } + }); + } + + this.widget.inputEditor.setDecorationsByType(decorationDescription, variableTextDecorationType, varDecorations); } } @@ -241,3 +272,58 @@ class SlashCommandCompletions extends Disposable { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); + +class VariableCompletions extends Disposable { + + private static readonly VariableNameDef = /@\w*/g; // MUST be using `g`-flag + + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatVariables', + triggerCharacters: ['@'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + return null; + } + + const varWord = getWordAtText(position.column, VariableCompletions.VariableNameDef, model.getLineContent(position.lineNumber), 0); + if (!varWord && model.getWordUntilPosition(position).word) { + // inside a "normal" word + 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); + } + + return { + suggestions: Array.from(this.chatVariablesService.getVariables()).map(v => { + const withAt = `@${v.name}`; + return { + label: withAt, + range: { insert, replace }, + insertText: withAt + ' ', + detail: v.description, + kind: CompletionItemKind.Text, // The icons are disabled here anyway, + }; + }) + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index a67ffb6896b..a5275042f81 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -221,30 +221,37 @@ margin: 0 0 8px 0; } -.interactive-session .interactive-input-and-toolbar { +.interactive-session .interactive-input-and-execute-toolbar { display: flex; box-sizing: border-box; cursor: text; margin: 0px 20px; background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); - border-radius: 2px; + border-radius: 4px; position: relative; - padding: 0 4px; + padding: 0 6px; + margin-bottom: 4px; align-items: center; justify-content: space-between; } -.interactive-session .interactive-input-and-toolbar.focused { +.interactive-session .interactive-input-and-side-toolbar { + display: flex; + gap: 6px; + align-items: center; +} + +.interactive-session .interactive-input-and-execute-toolbar.focused { border-color: var(--vscode-focusBorder); } -.interactive-session .interactive-input-and-toolbar .monaco-editor, -.interactive-session .interactive-input-and-toolbar .monaco-editor .monaco-editor-background { +.interactive-session .interactive-input-and-execute-toolbar .monaco-editor, +.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .monaco-editor-background { background-color: var(--vscode-input-background) !important; } -.interactive-session .interactive-input-and-toolbar .monaco-editor .cursors-layer { +.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .cursors-layer { padding-left: 4px; } @@ -411,20 +418,14 @@ .quick-input-widget .interactive-session .interactive-input-part { padding: 8px 6px 6px 6px; - border-top: 0; } .quick-input-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { bottom: 1px; } -.quick-input-widget .interactive-session .interactive-input-and-toolbar { +.quick-input-widget .interactive-session .interactive-input-and-execute-toolbar { margin: 0; - border-radius: 2px; -} - -.quick-input-widget .interactive-session .interactive-input-and-toolbar .chat-slash-command-content-widget { - top: 2px !important; } /* #endregion */ diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index a242829fd92..87b7ef10cc6 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -25,7 +25,7 @@ export interface IChatRequestModel { export interface IResponse { readonly value: (IMarkdownString | IChatResponseProgressFileTreeData)[]; onDidChangeValue: Event; - updateContent(responsePart: string | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean): void; + updateContent(responsePart: string | { treeData: IChatResponseProgressFileTreeData } | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean): void; asString(): string; } @@ -111,7 +111,7 @@ export class Response implements IResponse { this._responseData = Array.isArray(value) ? value : [value]; this._responseParts = Array.isArray(value) ? value.map((v) => ('value' in v ? { string: v } : { treeData: v })) : [{ string: value }]; this._responseRepr = this._responseParts.map((part) => { - if ('treeData' in part) { + if (isCompleteInteractiveProgressTreeData(part)) { return ''; } return part.string.value; @@ -122,12 +122,12 @@ export class Response implements IResponse { return this._responseRepr; } - updateContent(responsePart: string | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean): void { + updateContent(responsePart: string | { treeData: IChatResponseProgressFileTreeData } | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean): void { if (typeof responsePart === 'string') { const responsePartLength = this._responseParts.length - 1; const lastResponsePart = this._responseParts[responsePartLength]; - if (lastResponsePart.resolving === true || 'treeData' in lastResponsePart) { + if (lastResponsePart.resolving === true || isCompleteInteractiveProgressTreeData(lastResponsePart)) { // The last part is resolving or a tree data item, start a new part this._responseParts.push({ string: new MarkdownString(responsePart) }); } else { @@ -151,19 +151,21 @@ export class Response implements IResponse { this._updateRepr(quiet); } }); + } else if (isCompleteInteractiveProgressTreeData(responsePart)) { + this._responseParts.push(responsePart); } } private _updateRepr(quiet?: boolean) { this._responseData = this._responseParts.map(part => { - if ('treeData' in part) { + if (isCompleteInteractiveProgressTreeData(part)) { return part.treeData; } return part.string; }); this._responseRepr = this._responseParts.map(part => { - if ('treeData' in part) { + if (isCompleteInteractiveProgressTreeData(part)) { return ''; } return part.string.value; @@ -243,7 +245,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._id = 'response_' + ChatResponseModel.nextId++; } - updateContent(responsePart: string | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean) { + updateContent(responsePart: string | { treeData: IChatResponseProgressFileTreeData } | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean) { this._response.updateContent(responsePart, quiet); } @@ -547,7 +549,7 @@ export class ChatModel extends Disposable implements IChatModel { if ('content' in progress) { request.response.updateContent(progress.content, quiet); - } else if ('placeholder' in progress) { + } else if ('placeholder' in progress || isCompleteInteractiveProgressTreeData(progress)) { request.response.updateContent(progress, quiet); } else { request.setProviderRequestId(progress.requestId); @@ -692,3 +694,7 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { return this.session.responderAvatarIconUri; } } + +export function isCompleteInteractiveProgressTreeData(item: unknown): item is { treeData: IChatResponseProgressFileTreeData } { + return typeof item === 'object' && !!item && 'treeData' in item; +} diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 1bed4de4b2b..2812120dccd 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -5,11 +5,13 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatModel, ChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChat { id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about @@ -25,6 +27,7 @@ export interface IChat { export interface IChatRequest { session: IChat; message: string | IChatReplyFollowup; + variables: Record; } export interface IChatResponseErrorDetails { @@ -49,7 +52,7 @@ export interface IChatResponseProgressFileTreeData { } export type IChatProgress = - { content: string } | { requestId: string } | { placeholder: string; resolvedContent: Promise }; + { content: string } | { requestId: string } | { treeData: IChatResponseProgressFileTreeData } | { placeholder: string; resolvedContent: Promise }; export interface IPersistedChatState { } export interface IChatProvider { @@ -102,6 +105,7 @@ export interface IChatResponseCommandFollowup { commandId: string; args?: any[]; title: string; // supports codicon strings + when?: string; } export type IChatFollowup = IChatReplyFollowup | IChatResponseCommandFollowup; @@ -174,8 +178,9 @@ export interface IChatDynamicRequest { } export interface IChatCompleteResponse { - message: string; + message: string | (IMarkdownString | IChatResponseProgressFileTreeData)[]; errorDetails?: IChatResponseErrorDetails; + followups?: IChatFollowup[]; } export interface IChatDetail { diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a0a5c5f59b6..ae621f87f69 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -22,10 +22,11 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData, isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; -import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProgress, IChatProvider, IChatProviderInfo, IChatReplyFollowup, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProgress, IChatProvider, IChatProviderInfo, IChatReplyFollowup, IChatRequest, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService, IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -151,6 +152,7 @@ export class ChatService extends Disposable implements IChatService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService ) { super(); @@ -451,6 +453,9 @@ export class ChatService extends Disposable implements IChatService { this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`); } else if ('placeholder' in progress) { this.trace('sendRequest', `Provider returned placeholder for session ${model.sessionId}, ${progress.placeholder}`); + } else if (isCompleteInteractiveProgressTreeData(progress)) { + // This isn't exposed in API + this.trace('sendRequest', `Provider returned tree data for session ${model.sessionId}, ${progress.treeData.label}`); } else { this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`); } @@ -496,7 +501,17 @@ export class ChatService extends Disposable implements IChatService { rawResponse = { session: model.session! }; } else { - rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token); + const request: IChatRequest = { + session: model.session!, + message: resolvedCommand, + variables: {} + }; + + if (typeof request.message === 'string') { + request.variables = await this.chatVariablesService.resolveVariables(request.message, token); + } + + rawResponse = await provider.provideReply(request, progressCallback, token); } if (token.isCancellationRequested) { @@ -650,13 +665,22 @@ export class ChatService extends Disposable implements IChatService { await model.waitForInitialization(); const request = model.addRequest(message); - model.acceptResponseProgress(request, { - content: response.message, - }, true); + if (typeof response.message === 'string') { + model.acceptResponseProgress(request, { content: response.message }); + } else { + for (const part of response.message) { + const progress = isMarkdownString(part) ? { content: part.value } : { treeData: part }; + model.acceptResponseProgress(request, progress, true); + } + } model.setResponse(request, { session: model.session!, errorDetails: response.errorDetails, }); + if (response.followups !== undefined) { + model.setFollowups(request, response.followups); + } + model.completeResponse(request); } cancelCurrentRequestForSession(sessionId: string): void { diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts new file mode 100644 index 00000000000..f8fe3874ee9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { Iterable } from 'vs/base/common/iterator'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export interface IChatVariableData { + name: string; + description: string; +} + +export interface IChatRequestVariableValue { + level: 'short' | 'medium' | 'full'; + value: string; + description?: string; +} + +export interface IChatVariableResolver { + // TODO should we spec "zoom level" + (messageText: string, token: CancellationToken): Promise; +} + +export const IChatVariablesService = createDecorator('IChatVariablesService'); + +export interface IChatVariablesService { + _serviceBrand: undefined; + registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable; + getVariables(): Iterable>; + + /** + * Resolves all variables that occur in `prompt` + */ + resolveVariables(prompt: string, token: CancellationToken): Promise>; +} + +type ChatData = [data: IChatVariableData, resolver: IChatVariableResolver]; + +export class ChatVariablesService implements IChatVariablesService { + declare _serviceBrand: undefined; + + private _resolver = new Map(); + + constructor() { + } + + async resolveVariables(prompt: string, token: CancellationToken): Promise> { + const resolvedVariables: Record = {}; + const jobs: Promise[] = []; + + const regex = /(^|\s)@(\w+)(\s|$)/ig; + + let match: RegExpMatchArray | null; + while (match = regex.exec(prompt)) { + const candidate = match[2]; + const data = this._resolver.get(candidate.toLowerCase()); + if (data) { + jobs.push(data[1](prompt, token).then(value => { + if (value) { + resolvedVariables[candidate] = value; + } + }).catch(onUnexpectedExternalError)); + } + } + + await Promise.allSettled(jobs); + + return Promise.resolve(resolvedVariables); + } + + getVariables(): Iterable> { + return Iterable.map(this._resolver.values(), data => data[0]); + } + + registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable { + const key = data.name.toLowerCase(); + if (this._resolver.has(key)) { + throw new Error(`A chat variable with the name '${data.name}' already exists.`); + } + this._resolver.set(key, [data, resolver]); + return toDisposable(() => { + this._resolver.delete(key); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 85efedc27de..fadb4edc028 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -11,7 +11,7 @@ import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatRequestModel, IChatResponseModel, IChatModel, IChatWelcomeMessageContent, IResponse, Response } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatResponseErrorDetails, IChatReplyFollowup, IChatResponseCommandFollowup, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatResponseErrorDetails, IChatReplyFollowup, IChatResponseCommandFollowup, InteractiveSessionVoteDirection, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { @@ -56,12 +56,16 @@ export interface IChatRequestViewModel { currentRenderedHeight: number | undefined; } -export interface IChatResponseRenderData { +export interface IChatResponseMarkdownRenderData { renderedWordCount: number; lastRenderTime: number; isFullyRendered: boolean; } +export interface IChatResponseRenderData { + renderedParts: (IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData)[]; +} + export interface IChatLiveUpdateData { wordCountAfterLastUpdate: number; loadingStartTime: number; @@ -327,8 +331,9 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi const wordCount = countWords(_model.response.asString()); const timeDiff = now - this._contentUpdateTimings!.loadingStartTime; const impliedWordLoadRate = wordCount / (timeDiff / 1000); + const renderedWordCount = this.renderData?.renderedParts.reduce((acc, part) => acc += ('label' in part ? 0 : part.renderedWordCount), 0); if (!this.isComplete) { - this.trace('onDidChange', `Update- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${this.renderData?.renderedWordCount} words are rendered.`); + this.trace('onDidChange', `Update- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${renderedWordCount} words are rendered.`); this._contentUpdateTimings = { loadingStartTime: this._contentUpdateTimings!.loadingStartTime, lastUpdateTime: now, @@ -336,7 +341,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi impliedWordLoadRate }; } else { - this.trace(`onDidChange`, `Done- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${this.renderData?.renderedWordCount} words are rendered.`); + this.trace(`onDidChange`, `Done- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${renderedWordCount} words are rendered.`); } } else { this.logService.warn('ChatResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized'); @@ -344,10 +349,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi // new data -> new id, new content to render this._modelChangeCount++; - if (this.renderData) { - this.renderData.isFullyRendered = false; - this.renderData.lastRenderTime = Date.now(); - } this._onDidChange.fire(); })); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index a2f01a9d9ed..ca968ebf1c5 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -23,6 +23,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ChatVariablesService, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; class SimpleTestProvider extends Disposable implements IChatProvider { private static sessionId = 0; @@ -65,7 +66,8 @@ suite('Chat', () => { suiteSetup(async () => { instantiationService = new TestInstantiationService(new ServiceCollection( - [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)] + [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], + [IChatVariablesService, new SyncDescriptor(ChatVariablesService)] )); instantiationService.stub(IStorageService, storageService = new TestStorageService()); instantiationService.stub(ILogService, new NullLogService()); diff --git a/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts new file mode 100644 index 00000000000..a24ab711a95 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; + +suite('ChatVariables', function () { + + let service: ChatVariablesService; + + setup(function () { + service = new ChatVariablesService(); + }); + + + test('ChatVariables - resolveVariables', async function () { + service.registerVariable({ name: 'foo', description: 'bar' }, async () => ([{ level: 'full', value: 'farboo' }])); + service.registerVariable({ name: 'far', description: 'boo' }, async () => ([{ level: 'full', value: 'farboo' }])); + + { + const data = await service.resolveVariables('Hello @foo and@far', CancellationToken.None); + assert.strictEqual(Object.keys(data).length, 1); + assert.deepEqual(Object.keys(data).sort(), ['foo']); + } + { + const data = await service.resolveVariables('@foo Hello', CancellationToken.None); + assert.strictEqual(Object.keys(data).length, 1); + assert.deepEqual(Object.keys(data).sort(), ['foo']); + } + { + const data = await service.resolveVariables('Hello @foo', CancellationToken.None); + assert.strictEqual(Object.keys(data).length, 1); + assert.deepEqual(Object.keys(data).sort(), ['foo']); + } + { + const data = await service.resolveVariables('Hello @foo and@far @foo', CancellationToken.None); + assert.strictEqual(Object.keys(data).length, 1); + assert.deepEqual(Object.keys(data).sort(), ['foo']); + } + { + const data = await service.resolveVariables('Hello @foo and @far @foo', CancellationToken.None); + assert.strictEqual(Object.keys(data).length, 2); + assert.deepEqual(Object.keys(data).sort(), ['far', 'foo']); + } + { + const data = await service.resolveVariables('Hello @foo and @far @foo @unknown', CancellationToken.None); + assert.strictEqual(Object.keys(data).length, 2); + assert.deepEqual(Object.keys(data).sort(), ['far', 'foo']); + } + }); +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 44ba3928008..1e0d06ff9b8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -111,7 +111,7 @@ function createScreenReaderHelp(): IDisposable { onClose: () => { codeEditor.focus(); }, - options: { type: AccessibleViewType.Help, ariaLabel: localize('chat-help-label', "Diff editor accessibility help") } + options: { type: AccessibleViewType.Help } }); }, ContextKeyExpr.and( ContextKeyEqualsExpr.create('diffEditorVersion', 2), diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index d6be4855c01..0a16cc4822a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -273,7 +273,15 @@ Registry.as(QuickaccessExtensions.Quickaccess).registerQui contextKey: 'inFileSymbolsPicker', placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."), helpEntries: [ - { description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, commandId: GotoSymbolAction.ID }, - { description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY } + { + description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), + prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, + commandId: GotoSymbolAction.ID, + commandCenterOrder: 40 + }, + { + description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), + prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY + } ] }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts index b34d5058935..d4be0883444 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderControlCharacter.ts @@ -28,7 +28,7 @@ export class ToggleRenderControlCharacterAction extends Action2 { menu: { id: MenuId.MenubarAppearanceMenu, group: '4_editor', - order: 4 + order: 5 } }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts index af948e693be..e5255c219b5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleRenderWhitespace.ts @@ -28,7 +28,7 @@ class ToggleRenderWhitespaceAction extends Action2 { menu: { id: MenuId.MenubarAppearanceMenu, group: '4_editor', - order: 3 + order: 4 } }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts index 628d7e9677b..b2dd6a3cdea 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts @@ -154,7 +154,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort; const inlineChatId = 'inlineChat.start'; - let ariaLabel = `Ask ${providerName} to do something or start typing to dismiss.`; + let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; const handleClick = () => { this.telemetryService.publicLog2('workbenchActionExecuted', { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 3b1eebc2929..c0d1213a3c3 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -77,7 +77,11 @@ Registry.as(QuickAccessExtensions.Quickaccess).registerQui prefix: DEBUG_QUICK_ACCESS_PREFIX, contextKey: 'inLaunchConfigurationsPicker', placeholder: nls.localize('startDebugPlaceholder', "Type the name of a launch configuration to run."), - helpEntries: [{ description: nls.localize('startDebuggingHelp', "Start Debugging"), commandId: SELECT_AND_START_ID }] + helpEntries: [{ + description: nls.localize('startDebuggingHelp', "Start Debugging"), + commandId: SELECT_AND_START_ID, + commandCenterOrder: 50 + }] }); // Register quick access for debug console diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 9ffa5e8600f..0fad52f9739 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -120,7 +120,7 @@ export class DebugService implements IDebugService { this.disposables.add(this.adapterManager); this.configurationManager = this.instantiationService.createInstance(ConfigurationManager, this.adapterManager); this.disposables.add(this.configurationManager); - this.debugStorage = this.instantiationService.createInstance(DebugStorage); + this.debugStorage = this.disposables.add(this.instantiationService.createInstance(DebugStorage)); this.chosenEnvironments = this.debugStorage.loadChosenEnvironments(); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 13decde4682..2e2b758fc43 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -40,7 +40,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -export class DebugSession implements IDebugSession { +export class DebugSession implements IDebugSession, IDisposable { parentSession: IDebugSession | undefined; private _subId: string | undefined; @@ -52,7 +52,7 @@ export class DebugSession implements IDebugSession { private threads = new Map(); private threadIds: number[] = []; private cancellationMap = new Map(); - private rawListeners: IDisposable[] = []; + private rawListeners = new DisposableStore(); private fetchThreadsScheduler: RunOnceScheduler | undefined; private passFocusScheduler: RunOnceScheduler; private lastContinuedThreadId: number | undefined; @@ -103,14 +103,11 @@ export class DebugSession implements IDebugSession { this.repl = (this.parentSession as DebugSession).repl; } - const toDispose = new DisposableStore(); + const toDispose = this.rawListeners.add(new DisposableStore()); const replListener = toDispose.add(new MutableDisposable()); replListener.value = this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()); if (lifecycleService) { - toDispose.add(lifecycleService.onWillShutdown(() => { - this.shutdown(); - dispose(toDispose); - })); + toDispose.add(lifecycleService.onWillShutdown(() => this.shutdown())); } const compoundRoot = this._options.compoundRoot; @@ -950,7 +947,7 @@ export class DebugSession implements IDebugSession { return; } - this.rawListeners.push(this.raw.onDidInitialize(async () => { + this.rawListeners.add(this.raw.onDidInitialize(async () => { aria.status(localize('debuggingStarted', "Debugging started.")); const sendConfigurationDone = async () => { if (this.raw && this.raw.capabilities.supportsConfigurationDoneRequest) { @@ -976,7 +973,7 @@ export class DebugSession implements IDebugSession { })); const statusQueue = new Queue(); - this.rawListeners.push(this.raw.onDidStop(async event => { + this.rawListeners.add(this.raw.onDidStop(async event => { statusQueue.queue(async () => { this.passFocusScheduler.cancel(); this.stoppedDetails.push(event.body); @@ -1026,7 +1023,7 @@ export class DebugSession implements IDebugSession { }); })); - this.rawListeners.push(this.raw.onDidThread(event => { + this.rawListeners.add(this.raw.onDidThread(event => { statusQueue.queue(async () => { if (event.body.reason === 'started') { // debounce to reduce threadsRequest frequency and improve performance @@ -1034,7 +1031,7 @@ export class DebugSession implements IDebugSession { this.fetchThreadsScheduler = new RunOnceScheduler(() => { this.fetchThreads(); }, 100); - this.rawListeners.push(this.fetchThreadsScheduler); + this.rawListeners.add(this.fetchThreadsScheduler); } if (!this.fetchThreadsScheduler.isScheduled()) { this.fetchThreadsScheduler.schedule(); @@ -1052,7 +1049,7 @@ export class DebugSession implements IDebugSession { }); })); - this.rawListeners.push(this.raw.onDidTerminateDebugee(async event => { + this.rawListeners.add(this.raw.onDidTerminateDebugee(async event => { aria.status(localize('debuggingStopped', "Debugging stopped.")); if (event.body && event.body.restart) { await this.debugService.restartSession(this, event.body.restart); @@ -1061,14 +1058,14 @@ export class DebugSession implements IDebugSession { } })); - this.rawListeners.push(this.raw.onDidContinued(event => { + this.rawListeners.add(this.raw.onDidContinued(event => { statusQueue.queue(async () => { const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; if (typeof threadId === 'number') { this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId); const tokens = this.cancellationMap.get(threadId); this.cancellationMap.delete(threadId); - tokens?.forEach(t => t.cancel()); + tokens?.forEach(t => t.dispose(true)); } else { this.stoppedDetails = []; this.cancelAllRequests(); @@ -1082,7 +1079,7 @@ export class DebugSession implements IDebugSession { })); const outputQueue = new Queue(); - this.rawListeners.push(this.raw.onDidOutput(async event => { + this.rawListeners.add(this.raw.onDidOutput(async event => { const outputSeverity = event.body.category === 'stderr' ? Severity.Error : event.body.category === 'console' ? Severity.Warning : Severity.Info; // When a variables event is received, execute immediately to obtain the variables value #126967 @@ -1161,7 +1158,7 @@ export class DebugSession implements IDebugSession { }); })); - this.rawListeners.push(this.raw.onDidBreakpoint(event => { + this.rawListeners.add(this.raw.onDidBreakpoint(event => { const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined; const breakpoint = this.model.getBreakpoints().find(bp => bp.getIdFromAdapter(this.getId()) === id); const functionBreakpoint = this.model.getFunctionBreakpoints().find(bp => bp.getIdFromAdapter(this.getId()) === id); @@ -1216,30 +1213,30 @@ export class DebugSession implements IDebugSession { } })); - this.rawListeners.push(this.raw.onDidLoadedSource(event => { + this.rawListeners.add(this.raw.onDidLoadedSource(event => { this._onDidLoadedSource.fire({ reason: event.body.reason, source: this.getSource(event.body.source) }); })); - this.rawListeners.push(this.raw.onDidCustomEvent(event => { + this.rawListeners.add(this.raw.onDidCustomEvent(event => { this._onDidCustomEvent.fire(event); })); - this.rawListeners.push(this.raw.onDidProgressStart(event => { + this.rawListeners.add(this.raw.onDidProgressStart(event => { this._onDidProgressStart.fire(event); })); - this.rawListeners.push(this.raw.onDidProgressUpdate(event => { + this.rawListeners.add(this.raw.onDidProgressUpdate(event => { this._onDidProgressUpdate.fire(event); })); - this.rawListeners.push(this.raw.onDidProgressEnd(event => { + this.rawListeners.add(this.raw.onDidProgressEnd(event => { this._onDidProgressEnd.fire(event); })); - this.rawListeners.push(this.raw.onDidInvalidateMemory(event => { + this.rawListeners.add(this.raw.onDidInvalidateMemory(event => { this._onDidInvalidMemory.fire(event); })); - this.rawListeners.push(this.raw.onDidInvalidated(async event => { + this.rawListeners.add(this.raw.onDidInvalidated(async event => { if (!(event.body.areas && event.body.areas.length === 1 && (event.body.areas[0] === 'variables' || event.body.areas[0] === 'watch'))) { // If invalidated event only requires to update variables or watch, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 this.cancelAllRequests(); @@ -1253,7 +1250,7 @@ export class DebugSession implements IDebugSession { } })); - this.rawListeners.push(this.raw.onDidExitAdapter(event => this.onDidExitAdapter(event))); + this.rawListeners.add(this.raw.onDidExitAdapter(event => this.onDidExitAdapter(event))); } private onDidExitAdapter(event?: AdapterEndEvent): void { @@ -1280,6 +1277,11 @@ export class DebugSession implements IDebugSession { this._onDidChangeState.fire(); } + public dispose() { + this.cancelAllRequests(); + dispose(this.rawListeners); + } + //---- sources getSourceForUri(uri: URI): Source | undefined { @@ -1325,7 +1327,7 @@ export class DebugSession implements IDebugSession { } private cancelAllRequests(): void { - this.cancellationMap.forEach(tokens => tokens.forEach(t => t.cancel())); + this.cancellationMap.forEach(tokens => tokens.forEach(t => t.dispose(true))); this.cancellationMap.clear(); } diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 98bae97b4e1..50640fcf7e5 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -26,6 +26,7 @@ import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassem import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ILogService } from 'vs/platform/log/common/log'; +import { autorun } from 'vs/base/common/observable'; interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable { __vscodeVariableMenuContext?: string; @@ -1173,19 +1174,19 @@ export class ThreadAndSessionIds implements ITreeElement { } } -export class DebugModel implements IDebugModel { +export class DebugModel extends Disposable implements IDebugModel { private sessions: IDebugSession[]; private schedulers = new Map }>(); private breakpointsActivated = true; - private readonly _onDidChangeBreakpoints = new Emitter(); - private readonly _onDidChangeCallStack = new Emitter(); - private readonly _onDidChangeWatchExpressions = new Emitter(); - private breakpoints: Breakpoint[]; - private functionBreakpoints: FunctionBreakpoint[]; - private exceptionBreakpoints: ExceptionBreakpoint[]; - private dataBreakpoints: DataBreakpoint[]; - private watchExpressions: Expression[]; + private readonly _onDidChangeBreakpoints = this._register(new Emitter()); + private readonly _onDidChangeCallStack = this._register(new Emitter()); + private readonly _onDidChangeWatchExpressions = this._register(new Emitter()); + private breakpoints!: Breakpoint[]; + private functionBreakpoints!: FunctionBreakpoint[]; + private exceptionBreakpoints!: ExceptionBreakpoint[]; + private dataBreakpoints!: DataBreakpoint[]; + private watchExpressions!: Expression[]; private instructionBreakpoints: InstructionBreakpoint[]; constructor( @@ -1194,11 +1195,21 @@ export class DebugModel implements IDebugModel { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService ) { - this.breakpoints = debugStorage.loadBreakpoints(); - this.functionBreakpoints = debugStorage.loadFunctionBreakpoints(); - this.exceptionBreakpoints = debugStorage.loadExceptionBreakpoints(); - this.dataBreakpoints = debugStorage.loadDataBreakpoints(); - this.watchExpressions = debugStorage.loadWatchExpressions(); + super(); + + this._register(autorun(reader => { + this.breakpoints = debugStorage.breakpoints.read(reader); + this.functionBreakpoints = debugStorage.functionBreakpoints.read(reader); + this.exceptionBreakpoints = debugStorage.exceptionBreakpoints.read(reader); + this.dataBreakpoints = debugStorage.dataBreakpoints.read(reader); + this._onDidChangeBreakpoints.fire(undefined); + })); + + this._register(autorun(reader => { + this.watchExpressions = debugStorage.watchExpressions.read(reader); + this._onDidChangeWatchExpressions.fire(undefined); + })); + this.instructionBreakpoints = []; this.sessions = []; } diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index 1265ad5fa13..1f11c827a16 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -10,6 +10,8 @@ import { IEvaluate, IExpression, IDebugModel } from 'vs/workbench/contrib/debug/ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ILogService } from 'vs/platform/log/common/log'; +import { observableValue } from 'vs/base/common/observable'; +import { Disposable } from 'vs/base/common/lifecycle'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; @@ -19,13 +21,38 @@ const DEBUG_WATCH_EXPRESSIONS_KEY = 'debug.watchexpressions'; const DEBUG_CHOSEN_ENVIRONMENTS_KEY = 'debug.chosenenvironment'; const DEBUG_UX_STATE_KEY = 'debug.uxstate'; -export class DebugStorage { +export class DebugStorage extends Disposable { + public readonly breakpoints = observableValue('debugBreakpoints', this.loadBreakpoints()); + public readonly functionBreakpoints = observableValue('debugFunctionBreakpoints', this.loadFunctionBreakpoints()); + public readonly exceptionBreakpoints = observableValue('debugExceptionBreakpoints', this.loadExceptionBreakpoints()); + public readonly dataBreakpoints = observableValue('debugDataBreakpoints', this.loadDataBreakpoints()); + public readonly watchExpressions = observableValue('debugWatchExpressions', this.loadWatchExpressions()); + constructor( @IStorageService private readonly storageService: IStorageService, @ITextFileService private readonly textFileService: ITextFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService - ) { } + ) { + super(); + + this._register(storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this._store)(e => { + if (e.external) { + switch (e.key) { + case DEBUG_BREAKPOINTS_KEY: + return this.breakpoints.set(this.loadBreakpoints(), undefined); + case DEBUG_FUNCTION_BREAKPOINTS_KEY: + return this.functionBreakpoints.set(this.loadFunctionBreakpoints(), undefined); + case DEBUG_EXCEPTION_BREAKPOINTS_KEY: + return this.exceptionBreakpoints.set(this.loadExceptionBreakpoints(), undefined); + case DEBUG_DATA_BREAKPOINTS_KEY: + return this.dataBreakpoints.set(this.loadDataBreakpoints(), undefined); + case DEBUG_WATCH_EXPRESSIONS_KEY: + return this.watchExpressions.set(this.loadWatchExpressions(), undefined); + } + } + })); + } loadDebugUxState(): 'simple' | 'default' { return this.storageService.get(DEBUG_UX_STATE_KEY, StorageScope.WORKSPACE, 'default') as 'simple' | 'default'; @@ -35,7 +62,7 @@ export class DebugStorage { this.storageService.store(DEBUG_UX_STATE_KEY, value, StorageScope.WORKSPACE, StorageTarget.MACHINE); } - loadBreakpoints(): Breakpoint[] { + private loadBreakpoints(): Breakpoint[] { let result: Breakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((breakpoint: any) => { @@ -46,7 +73,7 @@ export class DebugStorage { return result || []; } - loadFunctionBreakpoints(): FunctionBreakpoint[] { + private loadFunctionBreakpoints(): FunctionBreakpoint[] { let result: FunctionBreakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_FUNCTION_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((fb: any) => { @@ -57,7 +84,7 @@ export class DebugStorage { return result || []; } - loadExceptionBreakpoints(): ExceptionBreakpoint[] { + private loadExceptionBreakpoints(): ExceptionBreakpoint[] { let result: ExceptionBreakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_EXCEPTION_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((exBreakpoint: any) => { @@ -68,7 +95,7 @@ export class DebugStorage { return result || []; } - loadDataBreakpoints(): DataBreakpoint[] { + private loadDataBreakpoints(): DataBreakpoint[] { let result: DataBreakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_DATA_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((dbp: any) => { @@ -79,7 +106,7 @@ export class DebugStorage { return result || []; } - loadWatchExpressions(): Expression[] { + private loadWatchExpressions(): Expression[] { let result: Expression[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_WATCH_EXPRESSIONS_KEY, StorageScope.WORKSPACE, '[]')).map((watchStoredData: { name: string; id: string }) => { diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 10161571d0a..9164b7458a8 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -4,20 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import * as dom from 'vs/base/browser/dom'; -import { Expression, Variable, Scope, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isWindows } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; +import { renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { isStatusbarInDebugMode } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { State } from 'vs/workbench/contrib/debug/common/debug'; -import { isWindows } from 'vs/base/common/platform'; +import { Expression, Scope, StackFrame, Thread, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; const $ = dom.$; suite('Debug - Base Debug View', () => { @@ -37,6 +38,8 @@ suite('Debug - Base Debug View', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('render view tree', () => { const container = $('.container'); const treeContainer = renderViewTree(container); @@ -132,9 +135,9 @@ suite('Debug - Base Debug View', () => { }); test('statusbar in debug mode', () => { - const model = createMockDebugModel(); - const session = createTestSession(model); - const session2 = createTestSession(model, undefined, { suppressDebugStatusbar: true }); + const model = createMockDebugModel(disposables); + const session = disposables.add(createTestSession(model)); + const session2 = disposables.add(createTestSession(model, undefined, { suppressDebugStatusbar: true })); assert.strictEqual(isStatusbarInDebugMode(State.Inactive, []), false); assert.strictEqual(isStatusbarInDebugMode(State.Initializing, [session]), false); assert.strictEqual(isStatusbarInDebugMode(State.Running, [session]), true); diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index e5db72a8eaa..db19f575765 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -4,24 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { URI as uri } from 'vs/base/common/uri'; -import { DebugModel, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; -import { getExpandedBodySize, getBreakpointMessageAndIcon } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { Range } from 'vs/editor/common/core/range'; -import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; -import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; -import { OverviewRulerLane } from 'vs/editor/common/model'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { URI as uri } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { OverviewRulerLane } from 'vs/editor/common/model'; import { LanguageService } from 'vs/editor/common/services/languageService'; -import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; -import { MockDebugService } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; +import { getBreakpointMessageAndIcon, getExpandedBodySize } from 'vs/workbench/contrib/debug/browser/breakpointsView'; +import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; +import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; +import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; +import { MockDebugService, MockDebugStorage } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]): void { +function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]) { let eventCount = 0; const toDispose = model.onDidChangeBreakpoints(e => { assert.strictEqual(e?.sessionOnly, false); @@ -37,22 +41,26 @@ function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakp assert.strictEqual((e!.added![i] as Breakpoint).lineNumber, data[i].lineNumber); } }); - model.addBreakpoints(uri, data); + const bps = model.addBreakpoints(uri, data); assert.strictEqual(eventCount, 1); + return bps; } suite('Debug - Breakpoints', () => { let model: DebugModel; - const disposables = new DisposableStore(); + let disposables: DisposableStore; setup(() => { - model = createMockDebugModel(); + disposables = new DisposableStore(); + model = createMockDebugModel(disposables); }); teardown(() => { - disposables.clear(); + disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + // Breakpoints test('simple', () => { @@ -175,7 +183,7 @@ suite('Debug - Breakpoints', () => { const modelUri = uri.file('/myfolder/myfile.js'); addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 5, enabled: true, condition: 'x > 5' }, { lineNumber: 10, enabled: false }]); const breakpoints = model.getBreakpoints(); - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); const data = new Map(); assert.strictEqual(breakpoints[0].lineNumber, 5); @@ -187,7 +195,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(breakpoints[0].lineNumber, 5); assert.strictEqual(breakpoints[1].lineNumber, 50); - const session2 = createTestSession(model); + const session2 = disposables.add(createTestSession(model)); const data2 = new Map(); data2.set(breakpoints[0].getId(), { verified: true, line: 100 }); data2.set(breakpoints[1].getId(), { verified: true, line: 500 }); @@ -212,7 +220,7 @@ suite('Debug - Breakpoints', () => { test('exception breakpoints', () => { let eventCount = 0; - model.onDidChangeBreakpoints(() => eventCount++); + disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); model.setExceptionBreakpointsForSession("session-id-1", [{ filter: 'uncaught', label: 'UNCAUGHT', default: true }]); assert.strictEqual(eventCount, 1); let exceptionBreakpoints = model.getExceptionBreakpointsForSession("session-id-1"); @@ -245,7 +253,7 @@ suite('Debug - Breakpoints', () => { test('exception breakpoints multiple sessions', () => { let eventCount = 0; - model.onDidChangeBreakpoints(() => eventCount++); + disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); model.setExceptionBreakpointsForSession("session-id-4", [{ filter: 'uncaught', label: 'UNCAUGHT', default: true }, { filter: 'caught', label: 'CAUGHT' }]); model.setExceptionBreakpointFallbackSession("session-id-4"); @@ -286,7 +294,7 @@ suite('Debug - Breakpoints', () => { test('instruction breakpoints', () => { let eventCount = 0; - model.onDidChangeBreakpoints(() => eventCount++); + disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); //address: string, offset: number, condition?: string, hitCondition?: string model.addInstructionBreakpoint('0xCCCCFFFF', 0); @@ -308,7 +316,7 @@ suite('Debug - Breakpoints', () => { test('data breakpoints', () => { let eventCount = 0; - model.onDidChangeBreakpoints(() => eventCount++); + disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); model.addDataBreakpoint('label', 'id', true, ['read'], 'read'); model.addDataBreakpoint('second', 'secondId', false, ['readWrite'], 'readWrite'); @@ -432,4 +440,31 @@ suite('Debug - Breakpoints', () => { textModel.dispose(); instantiationService.dispose(); }); + + test('updates when storage changes', () => { + const storage1 = disposables.add(new TestStorageService()); + const debugStorage1 = disposables.add(new MockDebugStorage(storage1)); + const model1 = disposables.add(new DebugModel(debugStorage1, { isDirty: (e: any) => false }, mockUriIdentityService, new NullLogService())); + + // 1. create breakpoints in the first model + const modelUri = uri.file('/myfolder/my file first.js'); + const first = [ + { lineNumber: 1, enabled: true, condition: 'x > 5' }, + { lineNumber: 2, column: 4, enabled: false }, + ]; + + addBreakpointsAndCheckEvents(model1, modelUri, first); + debugStorage1.storeBreakpoints(model1); + const stored = storage1.get('debug.breakpoint', StorageScope.WORKSPACE); + + // 2. hydrate a new model and ensure external breakpoints get applied + const storage2 = disposables.add(new TestStorageService()); + const model2 = disposables.add(new DebugModel(disposables.add(new MockDebugStorage(storage2)), { isDirty: (e: any) => false }, mockUriIdentityService, new NullLogService())); + storage2.store('debug.breakpoint', stored, StorageScope.WORKSPACE, StorageTarget.USER, /* external= */ true); + assert.deepStrictEqual(model2.getBreakpoints().map(b => b.getId()), model1.getBreakpoints().map(b => b.getId())); + + // 3. ensure non-external changes are ignored + storage2.store('debug.breakpoint', '[]', StorageScope.WORKSPACE, StorageTarget.USER, /* external= */ false); + assert.deepStrictEqual(model2.getBreakpoints().map(b => b.getId()), model1.getBreakpoints().map(b => b.getId())); + }); }); diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index c6158426173..9a9f2a09d63 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -4,24 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DebugModel, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import * as sinon from 'sinon'; -import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; -import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; -import { Range } from 'vs/editor/common/core/range'; -import { IDebugSessionOptions, State, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; -import { createDecorationsForStackFrame } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; -import { Constants } from 'vs/base/common/uint'; -import { getContext, getContextForContributedActions, getSpecificSourceName } from 'vs/workbench/contrib/debug/browser/callStackView'; -import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug/browser/debugService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { debugStackframe, debugStackframeFocused } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; +import { Constants } from 'vs/base/common/uint'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { Range } from 'vs/editor/common/core/range'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { MockRawSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; import { NullLogService } from 'vs/platform/log/common/log'; +import { createDecorationsForStackFrame } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; +import { getContext, getContextForContributedActions, getSpecificSourceName } from 'vs/workbench/contrib/debug/browser/callStackView'; +import { debugStackframe, debugStackframeFocused } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug/browser/debugService'; +import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; +import { IDebugService, IDebugSessionOptions, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DebugModel, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; +import { MockRawSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; const mockWorkspaceContextService = { getWorkspace: () => { @@ -70,22 +72,28 @@ function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFr suite('Debug - CallStack', () => { let model: DebugModel; let mockRawSession: MockRawSession; + let disposables: DisposableStore; setup(() => { - model = createMockDebugModel(); + disposables = new DisposableStore(); + model = createMockDebugModel(disposables); mockRawSession = new MockRawSession(); }); teardown(() => { + disposables.dispose(); sinon.restore(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + // Threads test('threads simple', () => { const threadId = 1; const threadName = 'firstThread'; const session = createTestSession(model); + disposables.add(session); model.addSession(session); assert.strictEqual(model.getSessions(true).length, 1); @@ -113,6 +121,7 @@ suite('Debug - CallStack', () => { // Add the threads const session = createTestSession(model); + disposables.add(session); model.addSession(session); session['raw'] = mockRawSession; @@ -193,6 +202,7 @@ suite('Debug - CallStack', () => { // Add the threads const session = createTestSession(model); + disposables.add(session); model.addSession(session); session['raw'] = mockRawSession; @@ -246,6 +256,7 @@ suite('Debug - CallStack', () => { const runningThreadName = 'runningThread'; const stoppedReason = 'breakpoint'; const session = createTestSession(model); + disposables.add(session); model.addSession(session); session['raw'] = mockRawSession; @@ -318,6 +329,7 @@ suite('Debug - CallStack', () => { test('stack frame get specific source name', () => { const session = createTestSession(model); + disposables.add(session); model.addSession(session); const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); @@ -327,6 +339,7 @@ suite('Debug - CallStack', () => { test('stack frame toString()', () => { const session = createTestSession(model); + disposables.add(session); const thread = new Thread(session, 'mockthread', 1); const firstSource = new Source({ name: 'internalModule.js', @@ -342,17 +355,17 @@ suite('Debug - CallStack', () => { }); test('debug child sessions are added in correct order', () => { - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); model.addSession(session); - const secondSession = createTestSession(model, 'mockSession2'); + const secondSession = disposables.add(createTestSession(model, 'mockSession2')); model.addSession(secondSession); - const firstChild = createTestSession(model, 'firstChild', { parentSession: session }); + const firstChild = disposables.add(createTestSession(model, 'firstChild', { parentSession: session })); model.addSession(firstChild); - const secondChild = createTestSession(model, 'secondChild', { parentSession: session }); + const secondChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: session })); model.addSession(secondChild); - const thirdSession = createTestSession(model, 'mockSession3'); + const thirdSession = disposables.add(createTestSession(model, 'mockSession3')); model.addSession(thirdSession); - const anotherChild = createTestSession(model, 'secondChild', { parentSession: secondSession }); + const anotherChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: secondSession })); model.addSession(anotherChild); const sessions = model.getSessions(); @@ -366,6 +379,7 @@ suite('Debug - CallStack', () => { test('decorations', () => { const session = createTestSession(model); + disposables.add(session); model.addSession(session); const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); let decorations = createDecorationsForStackFrame(firstStackFrame, true, false); @@ -398,6 +412,7 @@ suite('Debug - CallStack', () => { test('contexts', () => { const session = createTestSession(model); + disposables.add(session); model.addSession(session); const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); let context = getContext(firstStackFrame); @@ -436,8 +451,10 @@ suite('Debug - CallStack', () => { return State.Stopped; } }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService()); + disposables.add(session); const runningSession = createTestSession(model); + disposables.add(runningSession); model.addSession(runningSession); model.addSession(session); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index c3b5d88bf80..baade0af6d2 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -4,20 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { generateUuid } from 'vs/base/common/uuid'; -import { appendStylizedStringToContainer, handleANSIOutput, calcANSI8bitColor } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { Color, RGBA } from 'vs/base/common/color'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { TestThemeService, TestColorTheme } from 'vs/platform/theme/test/common/testThemeService'; -import { ansiColorMap } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; -import { DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; -import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; -import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestColorTheme, TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { appendStylizedStringToContainer, calcANSI8bitColor, handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; +import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; +import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; +import { ansiColorMap } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('Debug - ANSI Handling', () => { @@ -32,7 +33,7 @@ suite('Debug - ANSI Handling', () => { */ setup(() => { disposables = new DisposableStore(); - model = createMockDebugModel(); + model = createMockDebugModel(disposables); session = createTestSession(model); const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); @@ -50,6 +51,8 @@ suite('Debug - ANSI Handling', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('appendStylizedStringToContainer', () => { const root: HTMLSpanElement = document.createElement('span'); let child: Node; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts index 287c489a781..a63840db4e6 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts @@ -5,27 +5,28 @@ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { FileService } from 'vs/platform/files/common/fileService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NullLogService } from 'vs/platform/log/common/log'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugConfigurationProviderTriggerKind, IAdapterManager, IConfig, IDebugAdapterExecutable, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { TestHistoryService, TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { URI } from 'vs/base/common/uri'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; suite('debugConfigurationManager', () => { const configurationProviderType = 'custom-type'; let _debugConfigurationManager: ConfigurationManager; - const disposables = new DisposableStore(); + let disposables: DisposableStore; const adapterManager = { getDebugAdapterDescriptor(session: IDebugSession, config: IConfig): Promise { @@ -47,6 +48,7 @@ suite('debugConfigurationManager', () => { const configurationService = new TestConfigurationService(); setup(() => { + disposables = new DisposableStore(); const fileService = disposables.add(new FileService(new NullLogService())); const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection([IPreferencesService, preferencesService], [IConfigurationService, configurationService]))); _debugConfigurationManager = new ConfigurationManager( @@ -62,6 +64,10 @@ suite('debugConfigurationManager', () => { new ContextKeyService(configurationService)); }); + teardown(() => disposables.dispose()); + + ensureNoDisposablesAreLeakedInTestSuite(); + test('resolves configuration based on type', async () => { disposables.add(_debugConfigurationManager.registerDebugConfigurationProvider({ type: configurationProviderType, diff --git a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts index 62e17b6dcc2..f71a916f0c0 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { findExpressionInStackFrame } from 'vs/workbench/contrib/debug/browser/debugHover'; import type { IExpression, IScope } from 'vs/workbench/contrib/debug/common/debug'; @@ -13,9 +15,20 @@ import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callS import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; suite('Debug - Hover', () => { + let disposables: DisposableStore; + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + test('find expression in stack frame', async () => { - const model = createMockDebugModel(); - const session = createTestSession(model); + const model = createMockDebugModel(disposables); + const session = disposables.add(createTestSession(model)); const thread = new class extends Thread { public override getCallStack(): StackFrame[] { diff --git a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts index 499580f8416..7647820f3d0 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MemoryRangeType } from 'vs/workbench/contrib/debug/common/debug'; import { MemoryRegion } from 'vs/workbench/contrib/debug/common/debugModel'; import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; @@ -20,6 +21,8 @@ suite('Debug - Memory', () => { success: true, }; + ensureNoDisposablesAreLeakedInTestSuite(); + suite('MemoryRegion', () => { let memory: VSBuffer; let unreadable: number; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts index be735728c90..672ae3c3061 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts @@ -4,14 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { URI as uri } from 'vs/base/common/uri'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { isWindows } from 'vs/base/common/platform'; -import { mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; +import { URI as uri } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; suite('Debug - Source', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('from raw source', () => { const source = new Source({ name: 'zz', diff --git a/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts index 3b2b6003495..585497db087 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { formatPII, getExactExpressionStartAndEnd, getVisibleAndSorted } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfig } from 'vs/workbench/contrib/debug/common/debug'; +import { formatPII, getExactExpressionStartAndEnd, getVisibleAndSorted } from 'vs/workbench/contrib/debug/common/debugUtils'; suite('Debug - Utils', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('formatPII', () => { assert.strictEqual(formatPII('Foo Bar', false, {}), 'Foo Bar'); assert.strictEqual(formatPII('Foo {key} Bar', false, {}), 'Foo {key} Bar'); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts index 3b232aab21f..d42de786cb5 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; -import { StackFrame, Expression, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; -import { mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; -import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; import { NullLogService } from 'vs/platform/log/common/log'; +import { Expression, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; +import { mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; +import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; suite('Debug - View Model', () => { let model: ViewModel; @@ -19,6 +20,8 @@ suite('Debug - View Model', () => { model = new ViewModel(new MockContextKeyService()); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('focused stack frame', () => { assert.strictEqual(model.focusedStackFrame, undefined); assert.strictEqual(model.focusedThread, undefined); diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index 3e7ed14e6f8..a452e1a0e97 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { isWindows } from 'vs/base/common/platform'; -import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { URI } from 'vs/base/common/uri'; -import { ITunnelService } from 'vs/platform/tunnel/common/tunnel'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isWindows } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ITunnelService } from 'vs/platform/tunnel/common/tunnel'; +import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('Debug - Link Detector', () => { @@ -32,6 +33,8 @@ suite('Debug - Link Detector', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + /** * Assert that a given Element is an anchor element. * diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebugModel.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebugModel.ts index 29c7e61f148..9e736c215b1 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebugModel.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebugModel.ts @@ -3,15 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore } from 'vs/base/common/lifecycle'; import { NullLogService } from 'vs/platform/log/common/log'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { MockDebugStorage } from 'vs/workbench/contrib/debug/test/common/mockDebug'; import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; const fileService = new TestFileService(); export const mockUriIdentityService = new UriIdentityService(fileService); -export function createMockDebugModel(): DebugModel { - return new DebugModel(new MockDebugStorage(), { isDirty: (e: any) => false }, mockUriIdentityService, new NullLogService()); +export function createMockDebugModel(disposable: DisposableStore): DebugModel { + const storage = disposable.add(new TestStorageService()); + const debugStorage = disposable.add(new MockDebugStorage(storage)); + return disposable.add(new DebugModel(debugStorage, { isDirty: (e: any) => false }, mockUriIdentityService, new NullLogService())); } diff --git a/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts b/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts index 1ccdc5bcefe..465438ad2c5 100644 --- a/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock, mockObject } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -14,13 +16,24 @@ import { IDebugger } from 'vs/workbench/contrib/debug/common/debug'; import { MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; suite('RawDebugSession', () => { + let disposables: DisposableStore; + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + function createTestObjects() { const debugAdapter = new MockDebugAdapter(); const dbgr = mockObject()({ type: 'mock-debug' }); - new RawDebugSession( + const session = new RawDebugSession( debugAdapter, dbgr as any as IDebugger, 'sessionId', @@ -29,6 +42,9 @@ suite('RawDebugSession', () => { new (mock()), new (mock()), new (mock())); + disposables.add(session); + disposables.add(debugAdapter); + return { debugAdapter, dbgr }; } diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index a04f2ed1963..b1dbc030a48 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -5,30 +5,41 @@ import * as assert from 'assert'; -import severity from 'vs/base/common/severity'; -import { DebugModel, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; -import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; -import { ReplOutputElement, RawObjectReplElement, ReplEvaluationInput, ReplModel, ReplEvaluationResult, ReplGroup, ReplVariableElement } from 'vs/workbench/contrib/debug/common/replModel'; -import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { timeout } from 'vs/base/common/async'; -import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; -import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter'; import { TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { timeout } from 'vs/base/common/async'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import severity from 'vs/base/common/severity'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; +import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter'; +import { DebugModel, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplModel, ReplOutputElement, ReplVariableElement } from 'vs/workbench/contrib/debug/common/replModel'; +import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; +import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; import { MockDebugAdapter, MockRawSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; suite('Debug - REPL', () => { let model: DebugModel; let rawSession: MockRawSession; const configurationService = new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }); + let disposables: DisposableStore; + setup(() => { - model = createMockDebugModel(); + disposables = new DisposableStore(); + model = createMockDebugModel(disposables); rawSession = new MockRawSession(); }); + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + test('repl output', () => { - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); const repl = new ReplModel(configurationService); repl.appendToRepl(session, { output: 'first line\n', sev: severity.Error }); repl.appendToRepl(session, { output: 'second line ', sev: severity.Error }); @@ -86,7 +97,7 @@ suite('Debug - REPL', () => { }); test('repl output count', () => { - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); const repl = new ReplModel(configurationService); repl.appendToRepl(session, { output: 'first line\n', sev: severity.Info }); repl.appendToRepl(session, { output: 'first line\n', sev: severity.Info }); @@ -108,14 +119,14 @@ suite('Debug - REPL', () => { test('repl merging', () => { // 'mergeWithParent' should be ignored when there is no parent. - const parent = createTestSession(model, 'parent', { repl: 'mergeWithParent' }); - const child1 = createTestSession(model, 'child1', { parentSession: parent, repl: 'separate' }); - const child2 = createTestSession(model, 'child2', { parentSession: parent, repl: 'mergeWithParent' }); - const grandChild = createTestSession(model, 'grandChild', { parentSession: child2, repl: 'mergeWithParent' }); - const child3 = createTestSession(model, 'child3', { parentSession: parent }); + const parent = disposables.add(createTestSession(model, 'parent', { repl: 'mergeWithParent' })); + const child1 = disposables.add(createTestSession(model, 'child1', { parentSession: parent, repl: 'separate' })); + const child2 = disposables.add(createTestSession(model, 'child2', { parentSession: parent, repl: 'mergeWithParent' })); + const grandChild = disposables.add(createTestSession(model, 'grandChild', { parentSession: child2, repl: 'mergeWithParent' })); + const child3 = disposables.add(createTestSession(model, 'child3', { parentSession: parent })); let parentChanges = 0; - parent.onDidChangeReplElements(() => ++parentChanges); + disposables.add(parent.onDidChangeReplElements(() => ++parentChanges)); parent.appendToRepl({ output: '1\n', sev: severity.Info }); assert.strictEqual(parentChanges, 1); @@ -151,7 +162,7 @@ suite('Debug - REPL', () => { }); test('repl expressions', () => { - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); assert.strictEqual(session.getReplElements().length, 0); model.addSession(session); @@ -173,11 +184,11 @@ suite('Debug - REPL', () => { }); test('repl ordering', async () => { - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); model.addSession(session); const adapter = new MockDebugAdapter(); - const raw = new RawDebugSession(adapter, undefined!, '', '', undefined!, undefined!, undefined!, undefined!,); + const raw = disposables.add(new RawDebugSession(adapter, undefined!, '', '', undefined!, undefined!, undefined!, undefined!,)); session.initializeForTest(raw); await session.addReplExpression(undefined, 'before.1'); @@ -195,7 +206,7 @@ suite('Debug - REPL', () => { }); test('repl groups', async () => { - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); const repl = new ReplModel(configurationService); repl.appendToRepl(session, { output: 'first global line', sev: severity.Info }); @@ -233,7 +244,7 @@ suite('Debug - REPL', () => { }); test('repl filter', async () => { - const session = createTestSession(model); + const session = disposables.add(createTestSession(model)); const repl = new ReplModel(configurationService); const replFilter = new ReplFilter(); diff --git a/src/vs/workbench/contrib/debug/test/browser/watch.test.ts b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts index 5ae664ed05d..17fbff3be13 100644 --- a/src/vs/workbench/contrib/debug/test/browser/watch.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Expression, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { DebugModel, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; // Expressions @@ -19,13 +21,20 @@ function assertWatchExpressions(watchExpressions: Expression[], expectedName: st } suite('Debug - Watch', () => { - let model: DebugModel; + let disposables: DisposableStore; setup(() => { - model = createMockDebugModel(); + disposables = new DisposableStore(); + model = createMockDebugModel(disposables); }); + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + test('watch expressions', () => { assert.strictEqual(model.getWatchExpressions().length, 0); model.addWatchExpression('console'); diff --git a/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts index f4e048c7b1e..2246b7fa248 100644 --- a/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts @@ -5,9 +5,12 @@ import * as assert from 'assert'; import { timeout } from 'vs/base/common/async'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; suite('Debug - AbstractDebugAdapter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('event ordering', () => { let adapter: MockDebugAdapter; let output: string[]; diff --git a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts index 779f301a4eb..a39f590b616 100644 --- a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts @@ -5,12 +5,17 @@ import * as assert from 'assert'; import { DeferredPromise } from 'vs/base/common/async'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { mockObject } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { DebugModel, ExceptionBreakpoint, FunctionBreakpoint, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { MockDebugStorage } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('DebugModel', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + suite('FunctionBreakpoint', () => { test('Id is saved', () => { const fbp = new FunctionBreakpoint('function', true, 'hit condition', 'condition', 'log message'); @@ -42,7 +47,10 @@ suite('DebugModel', () => { }); fakeThread.getId.returns(1); - const model = new DebugModel(new MockDebugStorage(), { isDirty: (e: any) => false }, undefined!, new NullLogService()); + const disposable = new DisposableStore(); + const storage = disposable.add(new TestStorageService()); + const model = new DebugModel(disposable.add(new MockDebugStorage(storage)), { isDirty: (e: any) => false }, undefined!, new NullLogService()); + disposable.add(model); let top1Resolved = false; let whole1Resolved = false; @@ -70,6 +78,8 @@ suite('DebugModel', () => { await wholeStackDeferred.complete(); await result1.wholeCallStack; await result2.wholeCallStack; + + disposable.dispose(); }); }); }); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 5e8430947cf..30353a78a35 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -10,11 +10,11 @@ import { URI as uri } from 'vs/base/common/uri'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; import { NullLogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugger, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEvaluate, IExceptionBreakpoint, IExceptionInfo, IExpression, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, Expression, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; @@ -666,34 +666,7 @@ export class MockDebugAdapter extends AbstractDebugAdapter { export class MockDebugStorage extends DebugStorage { - constructor() { - super(undefined as any, undefined as any, undefined as any, new NullLogService()); + constructor(storageService: IStorageService) { + super(storageService, undefined as any, undefined as any, new NullLogService()); } - - override loadBreakpoints(): Breakpoint[] { - return []; - } - - override loadFunctionBreakpoints(): FunctionBreakpoint[] { - return []; - } - - override loadExceptionBreakpoints(): ExceptionBreakpoint[] { - return []; - - } - - override loadDataBreakpoints(): DataBreakpoint[] { - return []; - - } - - override loadWatchExpressions(): Expression[] { - return []; - - } - - override storeWatchExpressions(_watchExpressions: (IExpression & IEvaluate)[]): void { } - - override storeBreakpoints(_debugModel: IDebugModel): void { } } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 0f6e359a961..2ac22f1d0d7 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -206,14 +206,17 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } else if (shouldAutoResumeOnReload) { // The application has previously launched via a protocol URL Continue On flow const hasApplicationLaunchedFromContinueOnFlow = this.storageService.getBoolean(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION, false); + this.logService.info(`Prompting to enable cloud changes, has application previously launched from Continue On flow: ${hasApplicationLaunchedFromContinueOnFlow}`); const handlePendingEditSessions = () => { // display a badge in the accounts menu but do not prompt the user to sign in again + this.logService.info('Showing badge to enable cloud changes in accounts menu...'); this.updateAccountsMenuBadge(); this.pendingEditSessionsContext.set(true); // attempt a resume if we are in a pending state and the user just signed in const disposable = this.editSessionsStorageService.onDidSignIn(async () => { disposable.dispose(); + this.logService.info('Showing badge to enable cloud changes in accounts menu succeeded, resuming cloud changes...'); await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(undefined, true, undefined, undefined, progress)); this.storageService.remove(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION); this.environmentService.continueOn = undefined; @@ -227,8 +230,10 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo ) { // store the fact that we prompted the user this.storageService.store(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); + this.logService.info('Prompting to enable cloud changes...'); await this.editSessionsStorageService.initialize('read'); if (this.editSessionsStorageService.isSignedIn) { + this.logService.info('Prompting to enable cloud changes succeeded, resuming cloud changes...'); await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(undefined, true, undefined, undefined, progress)); } else { handlePendingEditSessions(); @@ -239,6 +244,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo ) { handlePendingEditSessions(); } + } else { + this.logService.debug('Auto resuming cloud changes disabled.'); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts index fb876ba670a..afa94056cd5 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts @@ -47,7 +47,7 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr this._register(this.hostService.onDidChangeFocus(hasFocus => hasFocus ? this.reloadVisibleTextFileEditors() : undefined)); // Lifecycle - this.lifecycleService.onDidShutdown(() => this.dispose()); + this._register(this.lifecycleService.onDidShutdown(() => this.dispose())); } //#region Text File: Ensure every dirty text and untitled file is opened in an editor diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 82c91b59496..30e70d2f05a 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -377,11 +377,16 @@ async function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEd // has 2 sides, we consider both, to support saving both sides. // We only allow this when saving, not for "Save As" and not if any // editor is untitled which would bring up a "Save As" dialog too. + // In addition, we require the secondary side to be modified to not + // trigger a touch operation unexpectedly. + // // See also https://github.com/microsoft/vscode/issues/4180 // See also https://github.com/microsoft/vscode/issues/106330 + // See also https://github.com/microsoft/vscode/issues/190210 if ( activeGroup.activeEditor instanceof SideBySideEditorInput && - !options?.saveAs && !(activeGroup.activeEditor.primary.hasCapability(EditorInputCapabilities.Untitled) || activeGroup.activeEditor.secondary.hasCapability(EditorInputCapabilities.Untitled)) + !options?.saveAs && !(activeGroup.activeEditor.primary.hasCapability(EditorInputCapabilities.Untitled) || activeGroup.activeEditor.secondary.hasCapability(EditorInputCapabilities.Untitled)) && + activeGroup.activeEditor.secondary.isModified() ) { editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.primary }); editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.secondary }); diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index e93695b3c16..56c7149a0ae 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -715,7 +715,7 @@ export class FileDownload { reject(canceled()); })); - disposables.add(listenStream(sourceStream, { + listenStream(sourceStream, { onData: data => { target.write(data.buffer); this.reportProgress(contents.name, contents.size, data.byteLength, operation); @@ -728,7 +728,7 @@ export class FileDownload { disposables.dispose(); resolve(); } - })); + }, token); }); } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 4f3d6065173..f6de1a29bc4 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -181,7 +181,7 @@ configurationRegistry.registerConfiguration({ }, [FILES_ASSOCIATIONS_CONFIG]: { 'type': 'object', - 'markdownDescription': nls.localize('associations', "Configure file associations to languages (for example `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed."), + 'markdownDescription': nls.localize('associations', "Configure [glob patterns](https://aka.ms/vscode-glob-patterns) of file associations to languages (for example `\"*.extension\": \"html\"`). Patterns will match on the absolute path of a file if they contain a path separator and will match on the name of the file otherwise. These have precedence over the default associations of the languages installed."), 'additionalProperties': { 'type': 'string' } diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index bc69b29a381..b0afe7cc554 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -77,6 +77,7 @@ suite('Files - TextFileEditorTracker', () => { instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); + disposables.add(editorService); instantiationService.stub(IEditorService, editorService); const accessor = instantiationService.createInstance(TestServiceAccessor); @@ -93,6 +94,7 @@ suite('Files - TextFileEditorTracker', () => { const resource = toResource.call(this, '/path/index.txt'); const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; + disposables.add(model); model.textEditorModel.setValue('Super Good'); assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Super Good'); @@ -141,6 +143,7 @@ suite('Files - TextFileEditorTracker', () => { } const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; + disposables.add(model); model.textEditorModel.setValue('Super Good'); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 8b6c004f074..96f58a5b6f8 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -18,7 +18,6 @@ import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibil import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { localize } from 'vs/nls'; import { Extensions, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; @@ -87,7 +86,7 @@ class InlineChatAccessibleViewContribution extends Disposable { controller.focus(); }, - options: { ariaLabel: localize('inlineChatAccessibleView', "Inline Chat Accessible View"), type: AccessibleViewType.View } + options: { type: AccessibleViewType.View } }); return true; }, ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 457fd102c76..fe2e0ccde7a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -42,7 +42,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: 2px 2px 2px 4px; + padding: 2px 2px 2px 6px; background-color: var(--vscode-inlineChatInput-background); cursor: text; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 104830cc852..f91de7e3ee8 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -100,7 +100,7 @@ export class InlineChatController implements IEditorContribution { private _messages = this._store.add(new Emitter()); - private readonly _sessionStore: DisposableStore = new DisposableStore(); + private readonly _sessionStore: DisposableStore = this._store.add(new DisposableStore()); private readonly _stashedSession: MutableDisposable = this._store.add(new MutableDisposable()); private _activeSession?: Session; private _strategy?: EditModeStrategy; @@ -146,6 +146,7 @@ export class InlineChatController implements IEditorContribution { } dispose(): void { + this._strategy?.dispose(); this._stashedSession.clear(); this.finishExistingSession(); this._store.dispose(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 434aea1f4d2..fcbb998dc79 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -385,6 +385,8 @@ export interface IInlineChatSessionService { // recordings(): readonly Recording[]; + + dispose(): void; } type SessionData = { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index bf16c0aec05..343749ce5c6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -230,7 +230,7 @@ export class InlineChatWidget { })); const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${InlineChatWidget._modelPool++}.txt` }); - this._inputModel = this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri); + this._inputModel = this._store.add(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); this._inputEditor.setModel(this._inputModel); // --- context keys @@ -359,13 +359,13 @@ export class InlineChatWidget { this._store.add(feedbackToolbar); // preview editors - this._previewDiffEditor = new IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget2, this._elements.previewDiff, { + this._previewDiffEditor = this._store.add(new IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget2, this._elements.previewDiff, { ..._previewEditorEditorOptions, onlyShowAccessibleDiffViewer: this._accessibilityService.isScreenReaderOptimized(), - }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor))); + }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor)))); this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); - this._previewCreateEditor = new IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor))); + this._previewCreateEditor = this._store.add(new IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor)))); this._elements.message.tabIndex = 0; this._elements.message.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); @@ -756,7 +756,7 @@ export class InlineChatWidget { this._slashCommandContentWidget.show(); // inject detail when otherwise empty - if (firstLine === `/${command.command} `) { + if (firstLine === `/${command.command}`) { newDecorations.push({ range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2), options: { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index ab5e4b27e1b..7a9beea7519 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -24,7 +24,6 @@ import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/co import { mock } from 'vs/base/test/common/mock'; import { Emitter, Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/arrays'; -import { timeout } from 'vs/base/common/async'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; @@ -114,11 +113,11 @@ suite('InteractiveChatController', function () { }] ); - instaService = workbenchInstantiationService(undefined, store).createChild(serviceCollection); - inlineChatSessionService = instaService.get(IInlineChatSessionService); + instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); - model = instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null); - editor = instantiateTestCodeEditor(instaService, model); + model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); + editor = store.add(instantiateTestCodeEditor(instaService, model)); store.add(inlineChatService.addProvider({ debugName: 'Unit Test', @@ -142,8 +141,6 @@ suite('InteractiveChatController', function () { }); teardown(function () { - editor.dispose(); - model.dispose(); store.clear(); ctrl?.dispose(); }); @@ -295,19 +292,8 @@ suite('InteractiveChatController', function () { wholeRange: new Range(3, 1, 3, 3) }; }, - async provideResponse(session, request) { - - // SLOW response - await timeout(50000); - - return { - type: InlineChatResponseType.EditorEdit, - id: Math.random(), - edits: [{ - range: new Range(1, 1, 1, 1), // EDIT happens outside of whole range - text: `${request.prompt}\n${request.prompt}` - }] - }; + provideResponse(session, request) { + return new Promise(() => { }); } }); store.add(d); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index af697829ec1..3dd004897d2 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -11,14 +11,12 @@ import Severity from 'vs/base/common/severity'; import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { ThemeColor, ThemeIcon } from 'vs/base/common/themables'; +import { ThemeIcon } from 'vs/base/common/themables'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_ERROR_ITEM_FOREGROUND, STATUS_BAR_WARNING_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from 'vs/workbench/services/statusbar/browser/statusbar'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { Link } from 'vs/platform/opener/browser/link'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -367,14 +365,11 @@ class EditorStatusContribution implements IWorkbenchContribution { private static _asStatusbarEntry(item: ILanguageStatus): IStatusbarEntry { - let color: ThemeColor | undefined; - let backgroundColor: ThemeColor | undefined; + let kind: StatusbarEntryKind | undefined; if (item.severity === Severity.Warning) { - color = themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND); - backgroundColor = themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND); + kind = 'warning'; } else if (item.severity === Severity.Error) { - color = themeColorFromId(STATUS_BAR_ERROR_ITEM_FOREGROUND); - backgroundColor = themeColorFromId(STATUS_BAR_ERROR_ITEM_BACKGROUND); + kind = 'error'; } return { @@ -383,8 +378,7 @@ class EditorStatusContribution implements IWorkbenchContribution { ariaLabel: item.accessibilityInfo?.label ?? item.label, role: item.accessibilityInfo?.role, tooltip: item.command?.tooltip || new MarkdownString(item.detail, { isTrusted: true, supportThemeIcons: true }), - color, - backgroundColor, + kind, command: item.command }; } diff --git a/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts b/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts new file mode 100644 index 00000000000..fc753a4b673 --- /dev/null +++ b/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import * as languages from 'vs/editor/common/languages'; + +CommandsRegistry.registerCommand( + '_executeMappedEditsProvider', + async ( + accessor: ServicesAccessor, + documentUri: URI, + codeBlocks: string[], + context: languages.MappedEditsContext + ): Promise => { + + const modelService = accessor.get(ITextModelService); + const langFeaturesService = accessor.get(ILanguageFeaturesService); + + const document = await modelService.createModelReference(documentUri); + + let result: languages.WorkspaceEdit | null = null; + + try { + const providers = langFeaturesService.mappedEditsProvider.ordered(document.object.textEditorModel); + + if (providers.length > 0) { + const mostRelevantProvider = providers[0]; + + const cancellationTokenSource = new CancellationTokenSource(); + + result = await mostRelevantProvider.provideMappedEdits( + document.object.textEditorModel, + codeBlocks, + context, + cancellationTokenSource.token + ); + } + } finally { + document.dispose(); + } + + return result; + } +); diff --git a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts index 9cb2c9dc650..b8334c948d6 100644 --- a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts +++ b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; import { MarkersModel, Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; import { groupBy } from 'vs/base/common/collections'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; class TestMarkersModel extends MarkersModel { @@ -27,6 +28,8 @@ class TestMarkersModel extends MarkersModel { suite('MarkersModel Test', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('marker ids are unique', function () { const marker1 = anErrorWithRange(3); const marker2 = anErrorWithRange(3); diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts index a32952de8ca..d411e9a4c1f 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts @@ -7,7 +7,6 @@ import * as assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IReader, transaction } from 'vs/base/common/observable'; import { isDefined } from 'vs/base/common/types'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { linesDiffComputers } from 'vs/editor/common/diff/linesDiffComputers'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; @@ -20,7 +19,8 @@ import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model import { MergeEditorTelemetry } from 'vs/workbench/contrib/mergeEditor/browser/telemetry'; suite('merge editor model', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + // todo: renable when failing case is found https://github.com/microsoft/vscode/pull/190444#issuecomment-1678151428 + // ensureNoDisposablesAreLeakedInTestSuite(); test('prepend line', async () => { await testMergeModel( diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts index ee2b1fdfa22..6f3874d0acd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts @@ -38,19 +38,36 @@ class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemP return; } - const languageId = cell.cellKind === CellKind.Markup ? - 'markdown' : - (this._languageService.getLanguageIdByLanguageName(cell.language) || cell.language); - const text = this._languageService.getLanguageName(languageId) || languageId; - const item = { - text, + const statusBarItems: INotebookCellStatusBarItem[] = []; + let displayLanguage = cell.language; + if (cell.cellKind === CellKind.Markup) { + displayLanguage = 'markdown'; + } else { + const registeredId = this._languageService.getLanguageIdByLanguageName(cell.language); + if (registeredId) { + displayLanguage = this._languageService.getLanguageName(displayLanguage) ?? displayLanguage; + } else { + // add unregistered lanugage warning item + const searchTooltip = localize('notebook.cell.status.searchLanguageExtensions', "Unknown cell language. Click to search for '{0}' extensions", cell.language); + statusBarItems.push({ + text: `$(dialog-warning)`, + command: { id: 'workbench.extensions.search', arguments: [`@tag:${cell.language}`], title: 'Search Extensions' }, + tooltip: searchTooltip, + alignment: CellStatusbarAlignment.Right, + priority: -Number.MAX_SAFE_INTEGER + 1 + }); + } + } + + statusBarItems.push({ + text: displayLanguage, command: CHANGE_CELL_LANGUAGE, tooltip: localize('notebook.cell.status.language', "Select Cell Language Mode"), alignment: CellStatusbarAlignment.Right, priority: -Number.MAX_SAFE_INTEGER - }; + }); return { - items: [item] + items: statusBarItems }; } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts index 1ec996bd137..576ded3d1f6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts @@ -10,9 +10,9 @@ import { isTextStreamMime } from 'vs/workbench/contrib/notebook/common/notebookC export async function copyCellOutput(mimeType: string | undefined, outputViewModel: ICellOutputViewModel, clipboardService: IClipboardService, logService: ILogService) { const cellOutput = outputViewModel.model; - const output = mimeType && CLIPBOARD_COMPATIBLE_MIMETYPES.includes(mimeType) ? + const output = mimeType && TEXT_BASED_MIMETYPES.includes(mimeType) ? cellOutput.outputs.find(output => output.mime === mimeType) : - cellOutput.outputs.find(output => CLIPBOARD_COMPATIBLE_MIMETYPES.includes(output.mime)); + cellOutput.outputs.find(output => TEXT_BASED_MIMETYPES.includes(output.mime)); mimeType = output?.mime; @@ -52,7 +52,7 @@ export async function copyCellOutput(mimeType: string | undefined, outputViewMod } } -export const CLIPBOARD_COMPATIBLE_MIMETYPES = [ +export const TEXT_BASED_MIMETYPES = [ 'text/latex', 'text/html', 'application/vnd.code.notebook.error', diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index e7c3a8d12c2..633dedb4a4b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -167,7 +167,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { text: nls.localize('kernel.select.label', "Select Kernel"), ariaLabel: nls.localize('kernel.select.label', "Select Kernel"), command: SELECT_KERNEL_ID, - backgroundColor: { id: 'statusBarItem.prominentBackground' } + kind: 'prominent' }, SELECT_KERNEL_ID, StatusbarAlignment.RIGHT, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts index 2b18ac4cca1..956222d7a36 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts @@ -5,37 +5,80 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { INotebookOutputActionContext, NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NOTEBOOK_CELL_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { ILogService } from 'vs/platform/log/common/log'; import { copyCellOutput } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copyToClipboard'; -registerAction2(class CopyCellOutputAction extends NotebookAction { +registerAction2(class CopyCellOutputAction extends Action2 { constructor() { - super( - { - id: COPY_OUTPUT_COMMAND_ID, - title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"), - menu: { - id: MenuId.NotebookOutputToolbar, - when: NOTEBOOK_CELL_HAS_OUTPUTS - }, - icon: icons.copyIcon, - }); + super({ + id: COPY_OUTPUT_COMMAND_ID, + title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"), + menu: { + id: MenuId.NotebookOutputToolbar, + when: NOTEBOOK_CELL_HAS_OUTPUTS + }, + category: NOTEBOOK_ACTIONS_CATEGORY, + icon: icons.copyIcon, + }); } - async runWithContext(accessor: ServicesAccessor, context: INotebookOutputActionContext): Promise { - const outputViewModel = context.outputViewModel; + async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel }): Promise { + const editorService = accessor.get(IEditorService); + const notebookEditor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!notebookEditor) { + return; + } + + let outputViewModel: ICellOutputViewModel | undefined; + if ('outputId' in outputContext && typeof outputContext.outputId === 'string') { + outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); + } else { + outputViewModel = outputContext.outputViewModel; + } + + if (!outputViewModel) { + return; + } const mimeType = outputViewModel.pickedMimeType?.mimeType; - const clipboardService = accessor.get(IClipboardService); - const logService = accessor.get(ILogService); - copyCellOutput(mimeType, outputViewModel, clipboardService, logService); + if (mimeType?.startsWith('image/')) { + const focusOptions = { skipReveal: true, outputId: outputViewModel.model.outputId }; + await notebookEditor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', focusOptions); + notebookEditor.copyOutputImage(outputViewModel); + } else { + const clipboardService = accessor.get(IClipboardService); + const logService = accessor.get(ILogService); + + copyCellOutput(mimeType, outputViewModel, clipboardService, logService); + } } + }); + +function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEditor): ICellOutputViewModel | undefined { + const notebookViewModel = notebookEditor.getViewModel(); + if (notebookViewModel) { + const codeCells = notebookViewModel.viewCells.filter(cell => cell.cellKind === CellKind.Code) as CodeCellViewModel[]; + for (const cell of codeCells) { + const output = cell.outputsViewModels.find(output => output.model.outputId === outputId); + if (output) { + return output; + } + } + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts index 55716426739..790e9e8bdc3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts @@ -54,7 +54,7 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi onClose: () => { editor.focus(); }, - options: { type: AccessibleViewType.Help, ariaLabel: 'Notebook accessibility help' } + options: { type: AccessibleViewType.Help } }); } @@ -114,10 +114,7 @@ export function showAccessibleOutput(accessibleViewService: IAccessibleViewServi notebookEditor?.setFocus(selections[0]); activePane?.focus(); }, - options: { - ariaLabel: localize('NotebookCellOutputAccessibleView', "Notebook Cell Output Accessible View"), - type: AccessibleViewType.View - } + options: { type: AccessibleViewType.View } }); return true; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 71bd138fdf8..587b8619587 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -151,6 +151,7 @@ export interface IFocusNotebookCellOptions { readonly skipReveal?: boolean; readonly focusEditorLine?: number; readonly minimalScrolling?: boolean; + readonly outputId?: string; } //#endregion @@ -549,6 +550,11 @@ export interface INotebookEditor { */ updateOutput(cell: ICellViewModel, output: IInsetRenderOutput, offset: number): Promise; + /** + * Copy the image in the specific cell output to the clipboard + */ + copyOutputImage(cellOutput: ICellOutputViewModel): Promise; + readonly onDidReceiveMessage: Event; /** diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index c418b358192..ba8ae7da1db 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2349,9 +2349,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } - if (!options?.skipReveal) { - this._webview.focusOutput(cell.id, this._webviewFocused); - } + const focusElementId = options?.outputId ?? cell.id; + this._webview.focusOutput(focusElementId, this._webviewFocused); cell.updateEditState(CellEditState.Preview, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Output; @@ -2793,6 +2792,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD }); } + async copyOutputImage(cellOutput: ICellOutputViewModel): Promise { + this._webview?.copyImage(cellOutput); + } + removeInset(output: ICellOutputViewModel) { this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { if (this._isDisposed || !this._webview) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index a118618482a..01fd2d2d227 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -35,7 +35,7 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookS import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { COPY_OUTPUT_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/controller/cellOutputActions'; import { CLEAR_CELL_OUTPUTS_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/controller/editActions'; -import { CLIPBOARD_COMPATIBLE_MIMETYPES } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; +import { TEXT_BASED_MIMETYPES } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -257,7 +257,7 @@ class CellOutputElement extends Disposable { } private shouldEnableCopy(mimeTypes: readonly IOrderedMimeType[]) { - if (!mimeTypes.find(mimeType => CLIPBOARD_COMPATIBLE_MIMETYPES.indexOf(mimeType.mimeType))) { + if (!mimeTypes.find(mimeType => TEXT_BASED_MIMETYPES.indexOf(mimeType.mimeType) || mimeType.mimeType.startsWith('image/'))) { return false; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index fe247bf9a85..b2359655e6d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -1074,10 +1074,12 @@ export class BackLayerWebView extends Themable { allowScripts: true, localResourceRoots: this.localResourceRootsCache, }, - extension: undefined + extension: undefined, + providedViewType: 'notebook.output' }); webview.setHtml(content); + webview.setContextKeyService(this.contextKeyService); return webview; } @@ -1543,6 +1545,13 @@ export class BackLayerWebView extends Themable { return; } + async copyImage(output: ICellOutputViewModel): Promise { + this._sendMessageToWebview({ + type: 'copyImage', + outputId: output.model.outputId + }); + } + removeInsets(outputs: readonly ICellOutputViewModel[]): void { if (this._disposed) { return; @@ -1599,7 +1608,7 @@ export class BackLayerWebView extends Themable { this.webview?.focus(); } - focusOutput(cellId: string, viewFocused: boolean) { + focusOutput(cellOrOutputId: string, viewFocused: boolean) { if (this._disposed) { return; } @@ -1610,7 +1619,7 @@ export class BackLayerWebView extends Themable { this._sendMessageToWebview({ type: 'focus-output', - cellId, + cellOrOutputId: cellOrOutputId, }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 35cac90d4aa..e74e173ae33 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -266,9 +266,14 @@ export interface IShowOutputMessage { readonly content?: ICreationContent; } +export interface ICopyImageMessage { + readonly type: 'copyImage'; + readonly outputId: string; +} + export interface IFocusOutputMessage { readonly type: 'focus-output'; - readonly cellId: string; + readonly cellOrOutputId: string; } export interface IAckOutputHeight { @@ -527,6 +532,7 @@ export type ToWebviewMessage = IClearMessage | IClearOutputRequestMessage | IHideOutputMessage | IShowOutputMessage | + ICopyImageMessage | IUpdateControllerPreloadsMessage | IUpdateRenderersMessage | IUpdateDecorationsMessage | diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index f7eb1abd38f..4715e64aa08 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -474,8 +474,8 @@ async function webviewPreloads(ctx: PreloadContext) { }); }; - function focusFirstFocusableOrContainerInOutput(cellId: string) { - const cellOutputContainer = document.getElementById(cellId); + function focusFirstFocusableOrContainerInOutput(cellOrOutputId: string) { + const cellOutputContainer = document.getElementById(cellOrOutputId); if (cellOutputContainer) { if (cellOutputContainer.contains(document.activeElement)) { return; @@ -1362,6 +1362,43 @@ async function webviewPreloads(ctx: PreloadContext) { }); }; + const copyOutputImage = async (outputId: string, retries = 5) => { + if (!document.hasFocus() && retries > 0) { + // copyImage can be called from outside of the webview, which means this function may be running whilst the webview is gaining focus. + // Since navigator.clipboard.write requires the document to be focused, we need to wait for focus. + // We cannot use a listener, as there is a high chance the focus is gained during the setup of the listener resulting in us missing it. + setTimeout(() => { copyOutputImage(outputId, retries - 1); }, 20); + return; + } + + try { + const image = document.getElementById(outputId)?.querySelector('img'); + if (image) { + await navigator.clipboard.write([new ClipboardItem({ + 'image/png': new Promise((resolve) => { + const canvas = document.createElement('canvas'); + if (canvas !== null) { + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + const context = canvas.getContext('2d'); + context?.drawImage(image, 0, 0); + } + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } + canvas.remove(); + }, 'image/png'); + }) + })]); + } else { + console.error('Could not find image element to copy for output with id', outputId); + } + } catch (e) { + console.error('Could not copy image:', e); + } + }; + window.addEventListener('message', async rawEvent => { const event = rawEvent as ({ data: webviewMessages.ToWebviewMessage }); @@ -1462,6 +1499,12 @@ async function webviewPreloads(ctx: PreloadContext) { }); break; } + case 'copyImage': { + + await copyOutputImage(event.data.outputId); + + break; + } case 'ack-dimension': { for (const { cellId, outputId, height } of event.data.updates) { viewModel.updateOutputHeight(cellId, outputId, height); @@ -1481,7 +1524,7 @@ async function webviewPreloads(ctx: PreloadContext) { break; } case 'focus-output': - focusFirstFocusableOrContainerInOutput(event.data.cellId); + focusFirstFocusableOrContainerInOutput(event.data.cellOrOutputId); break; case 'decorations': { let outputContainer = document.getElementById(event.data.cellId); @@ -2519,6 +2562,7 @@ async function webviewPreloads(ctx: PreloadContext) { ) { this.element = document.createElement('div'); this.element.classList.add('output_container'); + this.element.setAttribute('data-vscode-context', JSON.stringify({ 'preventDefaultContextMenuItems': true })); this.element.style.position = 'absolute'; this.element.style.overflow = 'hidden'; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index e32021676e5..73d9e68b70c 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -166,17 +166,10 @@ export class NotebookCellTextModel extends Disposable implements ICell { this._textModelDisposables.clear(); this._textModel = m; if (this._textModel) { - // Init language from text model - // The language defined in the cell might not be supported in the editor so the text model might be using the default fallback - // If so let's not modify the language - if (!(this._languageService.isRegisteredLanguageId(this.language) === false && (this._textModel.getLanguageId() === PLAINTEXT_LANGUAGE_ID || this._textModel.getLanguageId() === 'jupyter'))) { - this.language = this._textModel.getLanguageId(); - } + this.setRegisteredLanguage(this._languageService, this._textModel.getLanguageId(), this.language); // Listen to language changes on the model - this._textModelDisposables.add(this._textModel.onDidChangeLanguage(e => { - this.language = e.newLanguage; - })); + this._textModelDisposables.add(this._textModel.onDidChangeLanguage((e) => this.setRegisteredLanguage(this._languageService, e.newLanguage, this.language))); this._textModelDisposables.add(this._textModel.onWillDispose(() => this.textModel = undefined)); this._textModelDisposables.add(this._textModel.onDidChangeContent(() => { if (this._textModel) { @@ -191,6 +184,18 @@ export class NotebookCellTextModel extends Disposable implements ICell { } } + private setRegisteredLanguage(languageService: ILanguageService, newLanguage: string, currentLanguage: string) { + // The language defined in the cell might not be supported in the editor so the text model might be using the default fallback + // If so let's not modify the language + const isFallBackLanguage = (newLanguage === PLAINTEXT_LANGUAGE_ID || newLanguage === 'jupyter'); + if (!languageService.isRegisteredLanguageId(currentLanguage) && isFallBackLanguage) { + // notify to display warning, but don't change the language + this._onDidChangeLanguage.fire(currentLanguage); + } else { + this.language = newLanguage; + } + } + constructor( readonly uri: URI, public readonly handle: number, diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 4bdbae63271..0524f62bde4 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -158,8 +158,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { override isSaving(): boolean { const model = this._editorModelReference?.object; - if (!model || !model.isDirty() || model.hasErrorState) { - return false; // require the model to be dirty and not in an error state + if (!model || !model.isDirty() || model.hasErrorState || this.hasCapability(EditorInputCapabilities.Untitled)) { + return false; // require the model to be dirty, file-backed and not in an error state } // if a short auto save is configured, treat this as being saved diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 24d0b989f8f..fe7ce4b6848 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFilter, SettingMatchType } from 'vs/workbench/services/preferences/common/preferences'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFilter, SettingMatchType, ISettingMatch } from 'vs/workbench/services/preferences/common/preferences'; import { IRange } from 'vs/editor/common/core/range'; import { distinct } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; @@ -19,8 +19,9 @@ import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/exte import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ISemanticSimilarityService } from 'vs/workbench/services/semanticSimilarity/common/semanticSimilarityService'; +import { IAiRelatedInformationService, RelatedInformationType, SettingInformationResult } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; export interface IEndpointDetails { urlBase?: string; @@ -32,11 +33,11 @@ export class PreferencesSearchService extends Disposable implements IPreferences // @ts-expect-error disable remote search for now, ref https://github.com/microsoft/vscode/issues/172411 private _installedExtensions: Promise; + private _remoteSearchProvider: RemoteSearchProvider | undefined; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService ) { @@ -52,34 +53,22 @@ export class PreferencesSearchService extends Disposable implements IPreferences }); } - // @ts-expect-error disable remote search for now, ref https://github.com/microsoft/vscode/issues/172411 private get remoteSearchAllowed(): boolean { const workbenchSettings = this.configurationService.getValue().workbench.settings; - if (!workbenchSettings.enableNaturalLanguageSearch) { - return false; - } - - return !!this._endpoint.urlBase; + return workbenchSettings.enableNaturalLanguageSearch; } - private get _endpoint(): IEndpointDetails { - const workbenchSettings = this.configurationService.getValue().workbench.settings; - if (workbenchSettings.naturalLanguageSearchEndpoint) { - return { - urlBase: workbenchSettings.naturalLanguageSearchEndpoint, - key: workbenchSettings.naturalLanguageSearchKey - }; - } else { - return { - urlBase: this.productService.settingsSearchUrl - }; + getRemoteSearchProvider(filter: string, newExtensionsOnly = false): RemoteSearchProvider | undefined { + if (!this.remoteSearchAllowed) { + return undefined; } - } - getRemoteSearchProvider(filter: string, newExtensionsOnly = false): ISearchProvider | undefined { - // Disable for now because Bing search isn't supported anymore. - // Ref https://github.com/microsoft/vscode/issues/172411 - return undefined; + if (!this._remoteSearchProvider) { + this._remoteSearchProvider = this.instantiationService.createInstance(RemoteSearchProvider); + } + + this._remoteSearchProvider.setFilter(filter); + return this._remoteSearchProvider; } getLocalSearchProvider(filter: string): LocalSearchProvider { @@ -87,6 +76,15 @@ export class PreferencesSearchService extends Disposable implements IPreferences } } +function cleanFilter(filter: string): string { + // Remove " and : which are likely to be copypasted as part of a setting name. + // Leave other special characters which the user might want to search for. + return filter + .replace(/[":]/g, ' ') + .replace(/ /g, ' ') + .trim(); +} + export class LocalSearchProvider implements ISearchProvider { static readonly EXACT_MATCH_SCORE = 10000; static readonly START_SCORE = 1000; @@ -95,12 +93,7 @@ export class LocalSearchProvider implements ISearchProvider { private _filter: string, @IConfigurationService private readonly configurationService: IConfigurationService ) { - // Remove " and : which are likely to be copypasted as part of a setting name. - // Leave other special characters which the user might want to search for. - this._filter = this._filter - .replace(/[":]/g, ' ') - .replace(/ /g, ' ') - .trim(); + this._filter = cleanFilter(this._filter); } searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): Promise { @@ -305,4 +298,145 @@ export class SettingMatches { } } +class RemoteSearchKeysProvider { + private settingKeys: string[] = []; + private settingsRecord: Record = {}; + private currentPreferencesModel: ISettingsEditorModel | undefined; + + constructor( + private readonly aiRelatedInformationService: IAiRelatedInformationService, + private readonly semanticSimilarityService: ISemanticSimilarityService + ) { } + + updateModel(preferencesModel: ISettingsEditorModel) { + if (preferencesModel === this.currentPreferencesModel) { + return; + } + + this.currentPreferencesModel = preferencesModel; + this.refresh(); + } + + private refresh() { + this.settingKeys = []; + this.settingsRecord = {}; + + if ( + !this.currentPreferencesModel || + (!this.semanticSimilarityService.isEnabled() && !this.aiRelatedInformationService.isEnabled()) + ) { + return; + } + + for (const group of this.currentPreferencesModel.settingsGroups) { + if (group.id === 'mostCommonlyUsed') { + continue; + } + for (const section of group.sections) { + for (const setting of section.settings) { + this.settingKeys.push(setting.key); + this.settingsRecord[setting.key] = setting; + } + } + } + } + + getSettingKeys(): string[] { + return this.settingKeys; + } + + getSettingsRecord(): Record { + return this.settingsRecord; + } +} + +export class RemoteSearchProvider implements ISearchProvider { + private static readonly SEMANTIC_SIMILARITY_THRESHOLD = 0.73; + private static readonly SEMANTIC_SIMILARITY_MAX_PICKS = 15; + + private readonly _keysProvider: RemoteSearchKeysProvider; + private _filter: string = ''; + + constructor( + @ISemanticSimilarityService private readonly semanticSimilarityService: ISemanticSimilarityService, + @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService + ) { + this._keysProvider = new RemoteSearchKeysProvider(aiRelatedInformationService, semanticSimilarityService); + } + + setFilter(filter: string) { + this._filter = cleanFilter(filter); + } + + async searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken | undefined): Promise { + if ( + !this._filter || + (!this.semanticSimilarityService.isEnabled() && !this.aiRelatedInformationService.isEnabled())) { + return null; + } + + this._keysProvider.updateModel(preferencesModel); + const filterMatches = this.aiRelatedInformationService.isEnabled() + ? await this.getAiRelatedInformationItems(token) + : this.semanticSimilarityService.isEnabled() + ? await this.getSemanticSimilarityItems(token) + : []; + + return { + filterMatches + }; + } + + // TODO: Remove this when all semantic similarity providers are migrated to aiRelatedInformationService + private async getSemanticSimilarityItems(token?: CancellationToken | undefined) { + const settingKeys = this._keysProvider.getSettingKeys(); + const settingsRecord = this._keysProvider.getSettingsRecord(); + + const scores = await this.semanticSimilarityService.getSimilarityScore(this._filter, settingKeys, token ?? CancellationToken.None); + const filterMatches: ISettingMatch[] = []; + const sortedIndices = scores.map((_, i) => i).sort((a, b) => scores[b] - scores[a]); + let numOfSmartPicks = 0; + for (const i of sortedIndices) { + const score = scores[i]; + if (score < RemoteSearchProvider.SEMANTIC_SIMILARITY_THRESHOLD || numOfSmartPicks === RemoteSearchProvider.SEMANTIC_SIMILARITY_MAX_PICKS) { + break; + } + + const pick = settingKeys[i]; + filterMatches.push({ + setting: settingsRecord[pick], + matches: [settingsRecord[pick].range], + matchType: SettingMatchType.RemoteMatch, + score + }); + numOfSmartPicks++; + } + + return filterMatches; + } + + private async getAiRelatedInformationItems(token?: CancellationToken | undefined) { + const settingsRecord = this._keysProvider.getSettingsRecord(); + + const filterMatches: ISettingMatch[] = []; + const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation(this._filter, [RelatedInformationType.SettingInformation], token ?? CancellationToken.None) as SettingInformationResult[]; + relatedInformation.sort((a, b) => b.weight - a.weight); + + for (const info of relatedInformation) { + if (info.weight < RemoteSearchProvider.SEMANTIC_SIMILARITY_THRESHOLD || filterMatches.length === RemoteSearchProvider.SEMANTIC_SIMILARITY_MAX_PICKS) { + break; + } + const pick = info.setting; + filterMatches.push({ + setting: settingsRecord[pick], + matches: [settingsRecord[pick].range], + matchType: SettingMatchType.RemoteMatch, + score: info.weight + }); + } + + return filterMatches; + } +} + registerSingleton(IPreferencesSearchService, PreferencesSearchService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 1dac2bd1e62..d0666ca8be5 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -66,6 +66,7 @@ import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/co import { IProductService } from 'vs/platform/product/common/productService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; export const enum SettingsFocusContext { @@ -237,6 +238,7 @@ export class SettingsEditor2 extends EditorPane { @IProductService private readonly productService: IProductService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @IEditorProgressService private readonly editorProgressService: IEditorProgressService, ) { super(SettingsEditor2.ID, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); @@ -566,6 +568,9 @@ export class SettingsEditor2 extends EditorPane { this.searchWidget.updateAriaLabel(label); } + /** + * Render the header of the Settings editor, which includes the content above the splitview. + */ private createHeader(parent: HTMLElement): void { this.headerContainer = DOM.append(parent, $('.settings-header')); @@ -1622,19 +1627,20 @@ export class SettingsEditor2 extends EditorPane { // Trigger the local search. If it didn't find an exact match, trigger the remote search. const searchInProgress = this.searchInProgress = new CancellationTokenSource(); - return this.localSearchDelayer.trigger(() => { + return this.localSearchDelayer.trigger(async () => { if (searchInProgress && !searchInProgress.token.isCancellationRequested) { - return this.localFilterPreferences(query).then(result => { - if (result && !result.exactMatch) { - this.remoteSearchThrottle.trigger(() => { - return searchInProgress && !searchInProgress.token.isCancellationRequested ? - this.remoteSearchPreferences(query, this.searchInProgress!.token) : - Promise.resolve(); - }); - } - }); - } else { - return Promise.resolve(); + const progressRunner = this.editorProgressService.show(true); + const result = await this.localFilterPreferences(query); + if (result && !result.exactMatch) { + this.remoteSearchThrottle.trigger(async () => { + if (searchInProgress && !searchInProgress.token.isCancellationRequested) { + await this.remoteSearchPreferences(query, this.searchInProgress!.token); + } + progressRunner.done(); + }); + } else { + progressRunner.done(); + } } }); } @@ -1654,36 +1660,32 @@ export class SettingsEditor2 extends EditorPane { ]).then(() => { }); } - private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise { - return this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token).then(result => { - if (token && token.isCancellationRequested) { - // Handle cancellation like this because cancellation is lost inside the search provider due to async/await - return null; - } - - if (!this.searchResultModel) { - this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted()); - // Must be called before this.renderTree() - // to make sure the search results count is set. - this.searchResultModel.setResult(type, result); - this.tocTreeModel.currentSearchModel = this.searchResultModel; - this.onSearchModeToggled(); - } else { - this.searchResultModel.setResult(type, result); - this.tocTreeModel.update(); - } - - if (type === SearchResultIdx.Local) { - this.tocTree.setFocus([]); - this.viewState.filterToCategory = undefined; - this.tocTree.expandAll(); - } - - this.settingsTree.scrollTop = 0; - this.refreshTOCTree(); - this.renderTree(undefined, true); - return result; - }); + private async filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise { + const result = await this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token); + if (token?.isCancellationRequested) { + // Handle cancellation like this because cancellation is lost inside the search provider due to async/await + return null; + } + if (!this.searchResultModel) { + this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted()); + // Must be called before this.renderTree() + // to make sure the search results count is set. + this.searchResultModel.setResult(type, result); + this.tocTreeModel.currentSearchModel = this.searchResultModel; + this.onSearchModeToggled(); + } else { + this.searchResultModel.setResult(type, result); + this.tocTreeModel.update(); + } + if (type === SearchResultIdx.Local) { + this.tocTree.setFocus([]); + this.viewState.filterToCategory = undefined; + this.tocTree.expandAll(); + } + this.settingsTree.scrollTop = 0; + this.refreshTOCTree(); + this.renderTree(undefined, true); + return result; } private renderResultCountMessages() { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index c5278298cc5..bcd2604c8d7 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -382,7 +382,9 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } matchesAllTags(tagFilters?: Set): boolean { - if (!tagFilters || !tagFilters.size) { + if (!tagFilters?.size) { + // This setting, which may have tags, + // matches against a query with no tags. return true; } @@ -392,15 +394,9 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { this.inspectSelf(); } - if (this.tags) { - let hasFilteredTag = true; - tagFilters.forEach(tag => { - hasFilteredTag = hasFilteredTag && this.tags!.has(tag); - }); - return hasFilteredTag; - } else { - return false; - } + // Check that the filter tags are a subset of this setting's tags + return !!this.tags?.size && + Array.from(tagFilters).every(tag => this.tags!.has(tag)); } matchesScope(scope: SettingsTarget, isRemote: boolean): boolean { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 94dc42655cd..ad2a9b4a633 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -208,18 +208,13 @@ export abstract class AbstractListSettingWidget extend } const header = this.renderHeader(); - const ITEM_HEIGHT = 24; - let listHeight = ITEM_HEIGHT * this.model.items.length; if (header) { - listHeight += ITEM_HEIGHT; this.listElement.appendChild(header); } this.rowElements = this.model.items.map((item, i) => this.renderDataOrEditItem(item, i, focused)); this.rowElements.forEach(rowElement => this.listElement.appendChild(rowElement)); - - this.listElement.style.height = listHeight + 'px'; } protected createBasicSelectBox(value: IObjectEnumData): SelectBox { diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index dd837a29ec7..4df3b8e287f 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -36,6 +36,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ISemanticSimilarityService } from 'vs/workbench/services/semanticSimilarity/common/semanticSimilarityService'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ASK_QUICK_QUESTION_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; +import { CommandInformationResult, IAiRelatedInformationService, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider { @@ -75,6 +76,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce @IPreferencesService private readonly preferencesService: IPreferencesService, @IProductService private readonly productService: IProductService, @ISemanticSimilarityService private readonly semanticSimilarityService: ISemanticSimilarityService, + @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService, @IChatService private readonly chatService: IChatService ) { super({ @@ -137,7 +139,12 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce } protected hasAdditionalCommandPicks(filter: string, token: CancellationToken): boolean { - if (!this.useSemanticSimilarity || filter === '' || token.isCancellationRequested || !this.semanticSimilarityService.isEnabled()) { + if ( + !this.useSemanticSimilarity + || token.isCancellationRequested + || filter === '' + || !(this.semanticSimilarityService.isEnabled() || this.aiRelatedInformationService.isEnabled()) + ) { return false; } @@ -149,16 +156,49 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce return []; } - const format = allPicks.map(p => p.commandId); - let scores: number[]; + let additionalPicks; + try { // Wait a bit to see if the user is still typing await timeout(CommandsQuickAccessProvider.SEMANTIC_SIMILARITY_DEBOUNCE, token); - scores = await this.semanticSimilarityService.getSimilarityScore(filter, format, token); + additionalPicks = this.aiRelatedInformationService.isEnabled() + ? await this.getRelatedInformationPicks(allPicks, picksSoFar, filter, token) + : this.semanticSimilarityService.isEnabled() + ? await this.getSemanticSimilarityPicks(allPicks, picksSoFar, filter, token) + : []; } catch (e) { return []; } + if (additionalPicks.length) { + additionalPicks.unshift({ + type: 'separator', + label: localize('semanticSimilarity', "similar commands") + }); + } + + if (picksSoFar.length || additionalPicks.length) { + additionalPicks.push({ + type: 'separator' + }); + } + + const info = this.chatService.getProviderInfos()[0]; + if (info) { + additionalPicks.push({ + label: localize('askXInChat', "Ask {0}: {1}", info.displayName, filter), + commandId: ASK_QUICK_QUESTION_ACTION_ID, + args: [filter] + }); + } + + return additionalPicks; + } + + private async getSemanticSimilarityPicks(allPicks: ICommandQuickPick[], picksSoFar: ICommandQuickPick[], filter: string, token: CancellationToken) { + const format = allPicks.map(p => p.commandId); + const scores = await this.semanticSimilarityService.getSimilarityScore(filter, format, token); + if (token.isCancellationRequested) { return []; } @@ -181,26 +221,30 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce } } - if (numOfSmartPicks) { - additionalPicks.unshift({ - type: 'separator', - label: localize('semanticSimilarity', "similar commands") - }); - } + return additionalPicks; + } - if (picksSoFar.length || additionalPicks.length) { - additionalPicks.push({ - type: 'separator' - }); - } + private async getRelatedInformationPicks(allPicks: ICommandQuickPick[], picksSoFar: ICommandQuickPick[], filter: string, token: CancellationToken) { + const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation( + filter, + [RelatedInformationType.CommandInformation], + token + ) as CommandInformationResult[]; - const info = this.chatService.getProviderInfos()[0]; - if (info) { - additionalPicks.push({ - label: localize('askXInChat', "Ask {0}: {1}", info.displayName, filter), - commandId: ASK_QUICK_QUESTION_ACTION_ID, - args: [filter] - }); + // Sort by weight descending to get the most relevant results first + relatedInformation.sort((a, b) => b.weight - a.weight); + + const setOfPicksSoFar = new Set(picksSoFar.map(p => p.commandId)); + const additionalPicks = new Array(); + + for (const info of relatedInformation) { + if (info.weight < CommandsQuickAccessProvider.SEMANTIC_SIMILARITY_THRESHOLD || additionalPicks.length === CommandsQuickAccessProvider.SEMANTIC_SIMILARITY_MAX_PICKS) { + break; + } + const pick = allPicks.find(p => p.commandId === info.command && !setOfPicksSoFar.has(p.commandId)); + if (pick) { + additionalPicks.push(pick); + } } return additionalPicks; diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts index 85a5e04afd2..61a4af6eac8 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -24,7 +24,11 @@ quickAccessRegistry.registerQuickAccessProvider({ ctor: HelpQuickAccessProvider, prefix: HelpQuickAccessProvider.PREFIX, placeholder: localize('helpQuickAccessPlaceholder', "Type '{0}' to get help on the actions you can take from here.", HelpQuickAccessProvider.PREFIX), - helpEntries: [{ description: localize('helpQuickAccess', "Show all Quick Access Providers") }] + helpEntries: [{ + description: localize('helpQuickAccess', "Show all Quick Access Providers"), + commandCenterOrder: 70, + commandCenterLabel: localize('more', 'More') + }] }); quickAccessRegistry.registerQuickAccessProvider({ @@ -40,7 +44,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: CommandsQuickAccessProvider.PREFIX, contextKey: 'inCommandsPicker', placeholder: localize('commandsQuickAccessPlaceholder', "Type the name of a command to run."), - helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), commandId: ShowAllCommandsAction.ID }] + helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), commandId: ShowAllCommandsAction.ID, commandCenterOrder: 20 }] }); //#endregion diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 3130f1911ca..cab8b0d506e 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RunOnceScheduler, retry } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -45,7 +43,6 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IProductService } from 'vs/platform/product/common/productService'; import { DomEmitter } from 'vs/base/browser/event'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -53,20 +50,6 @@ import { infoIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcon import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; -export const STATUS_BAR_OFFLINE_BACKGROUND = registerColor('statusBar.offlineBackground', { - dark: '#6c1717', - light: '#6c1717', - hcDark: '#6c1717', - hcLight: '#6c1717' -}, nls.localize('statusBarOfflineBackground', "Status bar background color when the workbench is offline. The status bar is shown in the bottom of the window")); - -export const STATUS_BAR_OFFLINE_FOREGROUND = registerColor('statusBar.offlineForeground', { - dark: STATUS_BAR_HOST_NAME_FOREGROUND, - light: STATUS_BAR_HOST_NAME_FOREGROUND, - hcDark: STATUS_BAR_HOST_NAME_FOREGROUND, - hcLight: STATUS_BAR_HOST_NAME_FOREGROUND -}, nls.localize('statusBarOfflineForeground', "Status bar foreground color when the workbench is offline. The status bar is shown in the bottom of the window")); - type ActionGroup = [string, Array]; interface RemoteExtensionMetadata { @@ -558,8 +541,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr const properties: IStatusbarEntry = { name: nls.localize('remoteHost', "Remote Host"), - backgroundColor: themeColorFromId(this.networkState === 'offline' ? STATUS_BAR_OFFLINE_BACKGROUND : STATUS_BAR_HOST_NAME_BACKGROUND), - color: themeColorFromId(this.networkState === 'offline' ? STATUS_BAR_OFFLINE_FOREGROUND : STATUS_BAR_HOST_NAME_FOREGROUND), + kind: this.networkState === 'offline' ? 'offline' : 'remote', ariaLabel, text, showProgress, diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 037e25ff458..2289a2ece04 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -52,7 +52,7 @@ import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; -import { STATUS_BAR_HOST_NAME_BACKGROUND } from 'vs/workbench/common/theme'; +import { STATUS_BAR_REMOTE_ITEM_BACKGROUND } from 'vs/workbench/common/theme'; import { Codicon } from 'vs/base/common/codicons'; import { defaultButtonStyles, defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Attributes, CandidatePort, Tunnel, TunnelCloseReason, TunnelModel, TunnelSource, forwardedPortsViewEnabled, makeAddress, mapHasAddressLocalhostOrAllInterfaces, parseAddress } from 'vs/workbench/services/remote/common/tunnelModel'; @@ -218,8 +218,8 @@ class PortColumn implements ITableColumn { } class LocalAddressColumn implements ITableColumn { - readonly label: string = nls.localize('tunnel.addressColumn.label', "Local Address"); - readonly tooltip: string = nls.localize('tunnel.addressColumn.tooltip', "The address that the forwarded port is available at locally."); + readonly label: string = nls.localize('tunnel.addressColumn.label', "Forwarded Address"); + readonly tooltip: string = nls.localize('tunnel.addressColumn.tooltip', "The address that the forwarded port is available at."); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { @@ -744,7 +744,9 @@ export class TunnelPanel extends ViewPane { static readonly ID = TUNNEL_VIEW_ID; static readonly TITLE = nls.localize('remote.tunnel', "Ports"); + private panelContainer: HTMLElement | undefined; private table!: WorkbenchTable; + private tableDisposables: DisposableStore = this._register(new DisposableStore()); private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; private tunnelPrivacyContext: IContextKey; @@ -783,7 +785,7 @@ export class TunnelPanel extends ViewPane { this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService); this.tunnelPrivacyContext = TunnelPrivacyContextKey.bindTo(contextKeyService); this.tunnelPrivacyEnabledContext = TunnelPrivacyEnabledContextKey.bindTo(contextKeyService); - this.tunnelPrivacyEnabledContext.set(tunnelService.privacyOptions.length !== 0); + this.tunnelPrivacyEnabledContext.set(tunnelService.canChangePrivacy); this.tunnelProtocolContext = TunnelProtocolContextKey.bindTo(contextKeyService); this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService); this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService); @@ -806,6 +808,15 @@ export class TunnelPanel extends ViewPane { })); this.registerPrivacyActions(); + this._register(Event.once(this.tunnelService.onAddedTunnelProvider)(() => { + if (this.tunnelPrivacyEnabledContext.get() === false) { + this.tunnelPrivacyEnabledContext.set(tunnelService.canChangePrivacy); + updateActions(); + this.registerPrivacyActions(); + this.createTable(); + this.table.layout(this.height, this.width); + } + })); } private registerPrivacyActions() { @@ -827,11 +838,15 @@ export class TunnelPanel extends ViewPane { return this.remoteExplorerService.tunnelModel.forwarded.size + this.remoteExplorerService.tunnelModel.detected.size; } - protected override renderBody(container: HTMLElement): void { - super.renderBody(container); + private createTable(): void { + if (!this.panelContainer) { + return; + } + this.tableDisposables.clear(); - const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view')); - const widgetContainer = dom.append(panelContainer, dom.$('.customview-tree')); + dom.clearNode(this.panelContainer); + + const widgetContainer = dom.append(this.panelContainer, dom.$('.customview-tree')); widgetContainer.classList.add('ports-view'); widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); @@ -874,18 +889,19 @@ export class TunnelPanel extends ViewPane { const actionRunner: ActionRunner = new ActionRunner(); actionBarRenderer.actionRunner = actionRunner; - this._register(this.table.onContextMenu(e => this.onContextMenu(e, actionRunner))); - this._register(this.table.onMouseDblClick(e => this.onMouseDblClick(e))); - this._register(this.table.onDidChangeFocus(e => this.onFocusChanged(e))); - this._register(this.table.onDidChangeSelection(e => this.onSelectionChanged(e))); - this._register(this.table.onDidFocus(() => this.tunnelViewFocusContext.set(true))); - this._register(this.table.onDidBlur(() => this.tunnelViewFocusContext.set(false))); + this.tableDisposables.add(this.table); + this.tableDisposables.add(this.table.onContextMenu(e => this.onContextMenu(e, actionRunner))); + this.tableDisposables.add(this.table.onMouseDblClick(e => this.onMouseDblClick(e))); + this.tableDisposables.add(this.table.onDidChangeFocus(e => this.onFocusChanged(e))); + this.tableDisposables.add(this.table.onDidChangeSelection(e => this.onSelectionChanged(e))); + this.tableDisposables.add(this.table.onDidFocus(() => this.tunnelViewFocusContext.set(true))); + this.tableDisposables.add(this.table.onDidBlur(() => this.tunnelViewFocusContext.set(false))); const rerender = () => this.table.splice(0, Number.POSITIVE_INFINITY, this.viewModel.all); rerender(); let lastPortCount = this.portCount; - this._register(Event.debounce(this.viewModel.onForwardedPortsChanged, (_last, e) => e, 50)(() => { + this.tableDisposables.add(Event.debounce(this.viewModel.onForwardedPortsChanged, (_last, e) => e, 50)(() => { const newPortCount = this.portCount; if (((lastPortCount === 0) || (newPortCount === 0)) && (lastPortCount !== newPortCount)) { this._onDidChangeViewWelcomeState.fire(); @@ -894,7 +910,7 @@ export class TunnelPanel extends ViewPane { rerender(); })); - this._register(this.table.onMouseClick(e => { + this.tableDisposables.add(this.table.onMouseClick(e => { if (this.hasOpenLinkModifier(e.browserEvent)) { const selection = this.table.getSelectedElements(); if ((selection.length === 0) || @@ -904,7 +920,7 @@ export class TunnelPanel extends ViewPane { } })); - this._register(this.table.onDidOpen(e => { + this.tableDisposables.add(this.table.onDidOpen(e => { if (!e.element || (e.element.tunnelType !== TunnelType.Forwarded)) { return; } @@ -913,7 +929,7 @@ export class TunnelPanel extends ViewPane { } })); - this._register(this.remoteExplorerService.onDidChangeEditable(e => { + this.tableDisposables.add(this.remoteExplorerService.onDidChangeEditable(e => { this.isEditing = !!this.remoteExplorerService.getEditableData(e?.tunnel, e?.editId); this._onDidChangeViewWelcomeState.fire(); @@ -938,6 +954,13 @@ export class TunnelPanel extends ViewPane { })); } + protected override renderBody(container: HTMLElement): void { + super.renderBody(container); + + this.panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view')); + this.createTable(); + } + override shouldShowWelcome(): boolean { return this.viewModel.isEmpty() && !this.isEditing; } @@ -1048,7 +1071,11 @@ export class TunnelPanel extends ViewPane { } } + private height = 0; + private width = 0; protected override layoutBody(height: number, width: number): void { + this.height = height; + this.width = width; super.layoutBody(height, width); this.table.layout(height, width); } @@ -1775,9 +1802,9 @@ MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ })); registerColor('ports.iconRunningProcessForeground', { - light: STATUS_BAR_HOST_NAME_BACKGROUND, - dark: STATUS_BAR_HOST_NAME_BACKGROUND, - hcDark: STATUS_BAR_HOST_NAME_BACKGROUND, - hcLight: STATUS_BAR_HOST_NAME_BACKGROUND + light: STATUS_BAR_REMOTE_ITEM_BACKGROUND, + dark: STATUS_BAR_REMOTE_ITEM_BACKGROUND, + hcDark: STATUS_BAR_REMOTE_ITEM_BACKGROUND, + hcLight: STATUS_BAR_REMOTE_ITEM_BACKGROUND }, nls.localize('portWithRunningProcess.foreground', "The color of the icon for a port that has an associated running process.")); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index ccd7471df75..94fc88fe959 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -40,7 +40,7 @@ import { Schemas } from 'vs/base/common/network'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ResourceMap } from 'vs/base/common/map'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; -import { AnythingQuickAccessProviderRunOptions, DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess'; +import { AnythingQuickAccessProviderRunOptions, DefaultQuickAccessFilterValue, Extensions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess'; import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -52,11 +52,11 @@ import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { stripIcons } from 'vs/base/common/iconLabels'; -import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess'; -import { CommandsQuickAccessProvider } from 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess'; -import { DEBUG_QUICK_ACCESS_PREFIX } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { TasksQuickAccessProvider } from 'vs/workbench/contrib/tasks/browser/tasksQuickAccess'; import { Lazy } from 'vs/base/common/lazy'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ASK_QUICK_QUESTION_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; +import { IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; interface IAnythingQuickPickItem extends IPickerQuickAccessItem, IQuickPickItemWithResource { } @@ -187,6 +187,8 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider Registry.as(Extensions.Quickaccess)); private getHelpPicks(query: IPreparedQuery, token: CancellationToken, runOptions?: AnythingQuickAccessProviderRunOptions): IAnythingQuickPickItem[] { if (query.normalized) { return []; // If there's a filter, we don't show the help } - type IHelpAnythingQuickPickItem = IAnythingQuickPickItem & { prefix: string }; - const providers: Array = this.helpQuickAccess.getQuickAccessProviders(); - const mapOfProviders = new Map(); - for (const provider of providers) { - mapOfProviders.set(provider.prefix, provider); + type IHelpAnythingQuickPickItem = IAnythingQuickPickItem & { commandCenterOrder: number }; + const providers: IHelpAnythingQuickPickItem[] = this.lazyRegistry.value.getQuickAccessProviders() + .filter(p => p.helpEntries.some(h => h.commandCenterOrder !== undefined)) + .flatMap(provider => provider.helpEntries + .filter(h => h.commandCenterOrder !== undefined) + .map(helpEntry => { + const providerSpecificOptions: AnythingQuickAccessProviderRunOptions | undefined = { + ...runOptions, + includeHelp: provider.prefix === AnythingQuickAccessProvider.PREFIX ? false : runOptions?.includeHelp + }; + + const label = helpEntry.commandCenterLabel ?? helpEntry.description!; + return { + label, + description: helpEntry.prefix ?? provider.prefix, + commandCenterOrder: helpEntry.commandCenterOrder!, + keybinding: helpEntry.commandId ? this.keybindingService.lookupKeybinding(helpEntry.commandId) : undefined, + ariaLabel: localize('helpPickAriaLabel', "{0}, {1}", label, helpEntry.description), + accept: () => { + this.quickInputService.quickAccess.show(provider.prefix, { + preserveValue: true, + providerOptions: providerSpecificOptions + }); + } + }; + })); + + // TODO: There has to be a better place for this, but it's the first time we are adding a non-quick access provider + // to the command center, so for now, let's do this. + if (this.quickChatService.enabled) { + providers.push({ + label: localize('chat', "Open Quick Chat"), + commandCenterOrder: 30, + keybinding: this.keybindingService.lookupKeybinding(ASK_QUICK_QUESTION_ACTION_ID), + accept: () => this.quickChatService.toggle() + }); } - const importantProviders: Array = []; - const AddProvider = (prefix: string, modifications: Partial = {}) => { - if (mapOfProviders.has(prefix)) { - const provider = mapOfProviders.get(prefix)!; - - // We swap the label and description in this to emphasize the ability - // not the prefix. - provider.label = provider.description!; - provider.description = provider.prefix; - - // If the user chooses 'Go to File' the help should go away as if they were - // entering a new mode - const providerSpecificOptions: AnythingQuickAccessProviderRunOptions | undefined = { - ...runOptions, - includeHelp: provider.prefix === AnythingQuickAccessProvider.PREFIX ? false : runOptions?.includeHelp - }; - - importantProviders.push({ - ...mapOfProviders.get(prefix)!, - ...modifications, - accept: () => { - this.quickInputService.quickAccess.show(provider.prefix, { - preserveValue: true, - providerOptions: providerSpecificOptions - }); - } - }); - } - }; - - // TODO@TylerLeonhardt ideally this hardcoded list and hardcoded dependency moves - // into a provider model where when I register a quick access provider I can enlist - // for showing up in command center - - // Acts as the ordering too - AddProvider(AnythingQuickAccessProvider.PREFIX); - AddProvider(CommandsQuickAccessProvider.PREFIX); - AddProvider(GotoSymbolQuickAccessProvider.PREFIX); - AddProvider(DEBUG_QUICK_ACCESS_PREFIX); - AddProvider(TasksQuickAccessProvider.PREFIX); - AddProvider(HelpQuickAccessProvider.PREFIX, { - // More concise - label: localize('more', 'More') - }); - - return importantProviders; + return providers.sort((a, b) => a.commandCenterOrder - b.commandCenterOrder); } //#endregion diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts new file mode 100644 index 00000000000..ad3d5da68ff --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IMatch } from 'vs/base/common/filters'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; +import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IViewsService } from 'vs/workbench/common/views'; +import { searchDetailsIcon, searchOpenInFileIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { Match, MatchInNotebook, RenderableMatch, SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; +import { SearchView, getEditorSelectionFromMatch } from 'vs/workbench/contrib/search/browser/searchView'; +import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; +import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; +import { IPatternInfo, ISearchConfigurationProperties, ITextQuery, VIEW_ID } from 'vs/workbench/services/search/common/search'; + +export const TEXT_SEARCH_QUICK_ACCESS_PREFIX = '% '; + +const DEFAULT_TEXT_QUERY_BUILDER_OPTIONS: ITextQueryBuilderOptions = { + _reason: 'quickAccessSearch', + disregardIgnoreFiles: false, + disregardExcludeSettings: false, + onlyOpenEditors: false, + expandPatterns: true +}; + +const MAX_FILES_SHOWN = 30; +const MAX_RESULTS_PER_FILE = 10; + +export class TextSearchQuickAccess extends PickerQuickAccessProvider { + private queryBuilder: QueryBuilder; + private searchModel: SearchModel; + + private _getTextQueryBuilderOptions(charsPerLine: number): ITextQueryBuilderOptions { + return { + ...DEFAULT_TEXT_QUERY_BUILDER_OPTIONS, + ... { + extraFileResources: this._instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), + maxResults: this.searchConfig.maxResults ?? undefined, + isSmartCase: this.searchConfig.smartCase, + }, + + previewOptions: { + matchLines: 1, + charsPerLine + } + }; + } + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + @IEditorService private readonly _editorService: IEditorService, + @ILabelService private readonly _labelService: ILabelService, + @IViewsService private readonly _viewsService: IViewsService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(TEXT_SEARCH_QUICK_ACCESS_PREFIX, { canAcceptInBackground: true }); + + this.queryBuilder = this._instantiationService.createInstance(QueryBuilder); + this.searchModel = this._instantiationService.createInstance(SearchModel); + } + + private get searchConfig(): ISearchConfigurationProperties { + return this._configurationService.getValue('search'); + } + + private async doSearch(contentPattern: string): Promise { + + if (contentPattern === '') { + return undefined; + } + + const folderResources: IWorkspaceFolder[] = this._contextService.getWorkspace().folders; + const content: IPatternInfo = { + pattern: contentPattern, + }; + const charsPerLine = content.isRegExp ? 10000 : 1000; // from https://github.com/microsoft/vscode/blob/e7ad5651ac26fa00a40aa1e4010e81b92f655569/src/vs/workbench/contrib/search/browser/searchView.ts#L1508 + + const query: ITextQuery = this.queryBuilder.text(content, folderResources.map(folder => folder.uri), this._getTextQueryBuilderOptions(charsPerLine)); + + await this.searchModel.search(query, undefined); + return this.searchModel.searchResult; + } + + private moveToSearchViewlet(model: SearchModel, currentElem: RenderableMatch) { + // this function takes this._searchModel.searchResult and moves it to the search viewlet's search model. + // then, this._searchModel will construct a new (empty) SearchResult, and the search viewlet's search result will be disposed. + this._viewsService.openView(VIEW_ID, false); + const viewlet: SearchView | undefined = this._viewsService.getActiveViewWithId(VIEW_ID) as SearchView; + viewlet.importSearchResult(model); + + const viewer: WorkbenchCompressibleObjectTree | undefined = viewlet?.getControl(); + + viewer.setFocus([currentElem], getSelectionKeyboardEvent()); + viewer.setSelection([currentElem], getSelectionKeyboardEvent()); + viewer.reveal(currentElem); + } + + protected async _getPicks(contentPattern: string, disposables: DisposableStore, token: CancellationToken): Promise<(IQuickPickSeparator | IPickerQuickAccessItem)[]> { + + const searchResult = await this.doSearch(contentPattern); + + if (!searchResult) { + return []; + } + + const picks: Array = []; + + const matches = searchResult.matches(); + const files = matches.length > MAX_FILES_SHOWN ? matches.slice(0, MAX_FILES_SHOWN) : matches; + + for (let fileIndex = 0; fileIndex < matches.length; fileIndex++) { + if (fileIndex === MAX_FILES_SHOWN) { + + picks.push({ + type: 'separator', + }); + + picks.push({ + label: 'See More Files', + iconClass: ThemeIcon.asClassName(searchDetailsIcon), + accept: async () => { + this.moveToSearchViewlet(this.searchModel, matches[MAX_FILES_SHOWN]); + } + }); + break; + } + + const fileMatch = files[fileIndex]; + + const label = basenameOrAuthority(fileMatch.resource); + const description = this._labelService.getUriLabel(dirname(fileMatch.resource), { relative: true }); + + + picks.push({ + label, + type: 'separator', + tooltip: description, + buttons: [{ + iconClass: ThemeIcon.asClassName(searchOpenInFileIcon), + tooltip: localize('QuickSearchOpenInFile', "Open File") + }], + }); + + const results: Match[] = fileMatch.matches() ?? []; + for (let matchIndex = 0; matchIndex < results.length; matchIndex++) { + const element = results[matchIndex]; + + if (matchIndex === MAX_RESULTS_PER_FILE) { + picks.push({ + label: 'More', + iconClass: ThemeIcon.asClassName(searchDetailsIcon), + accept: async () => { + this.moveToSearchViewlet(this.searchModel, element); + } + }); + break; + } + const options = { + selection: getEditorSelectionFromMatch(element, this.searchModel), + revealIfVisible: true, + indexedCellOptions: element instanceof MatchInNotebook ? { cellIndex: element.cellIndex, selection: element.range } : undefined, + }; + const preview = element.preview(); + const previewText = ' ' + (preview.before + preview.inside + preview.after).trim().substring(0, 999); + const match: IMatch[] = [{ + start: preview.before.length + 2, + end: preview.before.length + preview.inside.length + 2 + }]; + picks.push({ + label: `${previewText}`, + highlights: { + label: match + }, + description: `${element.range().startLineNumber}:${element.range().startColumn}`, + ariaLabel: `Match at location ${element.range().startLineNumber}:${element.range().startColumn} - ${previewText}`, + accept: async () => { + await this._editorService.openEditor({ + resource: fileMatch.resource, + options + }, ACTIVE_GROUP); + }, + }); + } + + } + + return picks; + + } +} diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index ec2d3dd0103..0aa08eff74d 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -42,6 +42,8 @@ import 'vs/workbench/contrib/search/browser/searchActionsNav'; import 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; import 'vs/workbench/contrib/search/browser/searchActionsSymbol'; import 'vs/workbench/contrib/search/browser/searchActionsTopBar'; +import 'vs/workbench/contrib/search/browser/searchActionsTextQuickAccess'; +import { TEXT_SEARCH_QUICK_ACCESS_PREFIX, TextSearchQuickAccess } from 'vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, InstantiationType.Delayed); registerSingleton(ISearchHistoryService, SearchHistoryService, InstantiationType.Delayed); @@ -107,7 +109,11 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: AnythingQuickAccessProvider.PREFIX, placeholder: nls.localize('anythingQuickAccessPlaceholder', "Search files by name (append {0} to go to line or {1} to go to symbol)", AbstractGotoLineQuickAccessProvider.PREFIX, GotoSymbolQuickAccessProvider.PREFIX), contextKey: defaultQuickAccessContextKeyValue, - helpEntries: [{ description: nls.localize('anythingQuickAccess', "Go to File"), commandId: 'workbench.action.quickOpen' }] + helpEntries: [{ + description: nls.localize('anythingQuickAccess', "Go to File"), + commandId: 'workbench.action.quickOpen', + commandCenterOrder: 10 + }] }); quickAccessRegistry.registerQuickAccessProvider({ @@ -118,6 +124,14 @@ quickAccessRegistry.registerQuickAccessProvider({ helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), commandId: Constants.ShowAllSymbolsActionId }] }); +quickAccessRegistry.registerQuickAccessProvider({ + ctor: TextSearchQuickAccess, + prefix: TEXT_SEARCH_QUICK_ACCESS_PREFIX, + contextKey: 'inTextSearchPicker', + placeholder: nls.localize('textSearchPickerPlaceholder', "Search for text in your workspace files."), + helpEntries: [{ description: nls.localize('textSearchPickerHelp', "Show All Text Results (experimental)"), commandId: Constants.QuickTextSearchActionId }] +}); + // Configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTextQuickAccess.ts b/src/vs/workbench/contrib/search/browser/searchActionsTextQuickAccess.ts new file mode 100644 index 00000000000..ebf0558987d --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsTextQuickAccess.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { RenderableMatch } from 'vs/workbench/contrib/search/browser/searchModel'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { category } from 'vs/workbench/contrib/search/browser/searchActionsBase'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { TEXT_SEARCH_QUICK_ACCESS_PREFIX } from 'vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess'; + +registerAction2(class TextSearchQuickAccessAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.QuickTextSearchActionId, + title: { + value: nls.localize('quickTextSearch', "Quick Text Search"), + original: 'Quick Text Search' + }, + category, + menu: [{ + id: MenuId.SearchContext, + group: 'search_2', + order: 1 + }], + f1: true + }); + + } + + override async run(accessor: ServicesAccessor, match: RenderableMatch | undefined): Promise { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.quickAccess.show(TEXT_SEARCH_QUICK_ACCESS_PREFIX); + } +}); diff --git a/src/vs/workbench/contrib/search/browser/searchIcons.ts b/src/vs/workbench/contrib/search/browser/searchIcons.ts index 7d64b527301..34ab891e7ad 100644 --- a/src/vs/workbench/contrib/search/browser/searchIcons.ts +++ b/src/vs/workbench/contrib/search/browser/searchIcons.ts @@ -27,3 +27,4 @@ export const searchStopIcon = registerIcon('search-stop', Codicon.searchStop, lo export const searchViewIcon = registerIcon('search-view-icon', Codicon.search, localize('searchViewIcon', 'View icon of the search view.')); export const searchNewEditorIcon = registerIcon('search-new-editor', Codicon.newFile, localize('searchNewEditorIcon', 'Icon for the action to open a new search editor.')); +export const searchOpenInFileIcon = registerIcon('search-open-in-file', Codicon.goToFile, localize('searchOpenInFile', 'Icon for the action to go to the file of the current search result.')); diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 81920160a6c..82bf57c7636 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -94,7 +94,7 @@ export class Match { after = this._oneLinePreviewText.substring(this._rangeInPreviewText.endColumn - 1); before = lcut(before, 26); - before = before.trimLeft(); + before = before.trimStart(); let charsRemaining = Match.MAX_PREVIEW_CHARS - before.length; inside = inside.substr(0, charsRemaining); @@ -940,7 +940,7 @@ export class FolderMatch extends Disposable { protected _index: number, protected _query: ITextQuery, private _parent: SearchResult | FolderMatch, - private _searchModel: SearchModel, + private _searchResult: SearchResult, private _closestRoot: FolderMatchWorkspaceRoot | null, @IReplaceService private readonly replaceService: IReplaceService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @@ -957,7 +957,7 @@ export class FolderMatch extends Disposable { } get searchModel(): SearchModel { - return this._searchModel; + return this._searchResult.searchModel; } get showHighlights(): boolean { @@ -1033,7 +1033,7 @@ export class FolderMatch extends Disposable { } public createIntermediateFolderMatch(resource: URI, id: string, index: number, query: ITextQuery, baseWorkspaceFolder: FolderMatchWorkspaceRoot): FolderMatchWithResource { - const folderMatch = this.instantiationService.createInstance(FolderMatchWithResource, resource, id, index, query, this, this._searchModel, baseWorkspaceFolder); + const folderMatch = this.instantiationService.createInstance(FolderMatchWithResource, resource, id, index, query, this, this._searchResult, baseWorkspaceFolder); this.configureIntermediateMatch(folderMatch); this.doAddFolder(folderMatch); return folderMatch; @@ -1313,13 +1313,13 @@ export class FolderMatchWithResource extends FolderMatch { protected _normalizedResource: Lazy; - constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult | FolderMatch, _searchModel: SearchModel, _closestRoot: FolderMatchWorkspaceRoot | null, + constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult | FolderMatch, _searchResult: SearchResult, _closestRoot: FolderMatchWorkspaceRoot | null, @IReplaceService replaceService: IReplaceService, @IInstantiationService instantiationService: IInstantiationService, @ILabelService labelService: ILabelService, @IUriIdentityService uriIdentityService: IUriIdentityService ) { - super(_resource, _id, _index, _query, _parent, _searchModel, _closestRoot, replaceService, instantiationService, labelService, uriIdentityService); + super(_resource, _id, _index, _query, _parent, _searchResult, _closestRoot, replaceService, instantiationService, labelService, uriIdentityService); this._normalizedResource = new Lazy(() => this.uriIdentityService.extUri.removeTrailingPathSeparator(this.uriIdentityService.extUri.normalizePath( this.resource))); } @@ -1337,13 +1337,13 @@ export class FolderMatchWithResource extends FolderMatch { * FolderMatchWorkspaceRoot => folder for workspace root */ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { - constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult, _searchModel: SearchModel, + constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult, @IReplaceService replaceService: IReplaceService, @IInstantiationService instantiationService: IInstantiationService, @ILabelService labelService: ILabelService, @IUriIdentityService uriIdentityService: IUriIdentityService ) { - super(_resource, _id, _index, _query, _parent, _searchModel, null, replaceService, instantiationService, labelService, uriIdentityService); + super(_resource, _id, _index, _query, _parent, _parent, null, replaceService, instantiationService, labelService, uriIdentityService); } private normalizedUriParent(uri: URI): URI { @@ -1409,14 +1409,14 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { * FolderMatch => required resource (normal folder node) */ export class FolderMatchNoRoot extends FolderMatch { - constructor(_id: string, _index: number, _query: ITextQuery, _parent: SearchResult | FolderMatch, _searchModel: SearchModel, + constructor(_id: string, _index: number, _query: ITextQuery, _parent: SearchResult, @IReplaceService replaceService: IReplaceService, @IInstantiationService instantiationService: IInstantiationService, @ILabelService labelService: ILabelService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { - super(null, _id, _index, _query, _parent, _searchModel, null, replaceService, instantiationService, labelService, uriIdentityService); + super(null, _id, _index, _query, _parent, _parent, null, replaceService, instantiationService, labelService, uriIdentityService); } createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string): FileMatch { @@ -1570,7 +1570,7 @@ function createParentList(element: RenderableMatch): RenderableMatch[] { export class SearchResult extends Disposable { private _onChange = this._register(new PauseableEmitter({ - merge: this.mergeEvents + merge: mergeSearchResultEvents })); readonly onChange: Event = this._onChange.event; private _folderMatches: FolderMatchWorkspaceRoot[] = []; @@ -1585,7 +1585,7 @@ export class SearchResult extends Disposable { private _onDidChangeModelListener: IDisposable | undefined; constructor( - private _searchModel: SearchModel, + public searchModel: SearchModel, @IReplaceService private readonly replaceService: IReplaceService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @@ -1685,27 +1685,6 @@ export class SearchResult extends Disposable { this._query = query; } - private mergeEvents(events: IChangeEvent[]): IChangeEvent { - const retEvent: IChangeEvent = { - elements: [], - added: false, - removed: false, - }; - events.forEach((e) => { - if (e.added) { - retEvent.added = true; - } - - if (e.removed) { - retEvent.removed = true; - } - - retEvent.elements = retEvent.elements.concat(e.elements); - }); - - return retEvent; - } - private onDidAddNotebookEditorWidget(widget: NotebookEditorWidget): void { this._onWillChangeModelListener?.dispose(); @@ -1744,20 +1723,17 @@ export class SearchResult extends Disposable { } private _createBaseFolderMatch(resource: URI | null, id: string, index: number, query: ITextQuery): FolderMatch { - let folderMatch; + let folderMatch: FolderMatch; if (resource) { - folderMatch = this.instantiationService.createInstance(FolderMatchWorkspaceRoot, resource, id, index, query, this, this._searchModel); + folderMatch = this.instantiationService.createInstance(FolderMatchWorkspaceRoot, resource, id, index, query, this); } else { - folderMatch = this.instantiationService.createInstance(FolderMatchNoRoot, id, index, query, this, this._searchModel); + folderMatch = this.instantiationService.createInstance(FolderMatchNoRoot, id, index, query, this); } const disposable = folderMatch.onChange((event) => this._onChange.fire(event)); folderMatch.onDispose(() => disposable.dispose()); return folderMatch; } - get searchModel(): SearchModel { - return this._searchModel; - } add(allRaw: IFileMatch[], searchInstanceID: string, silent: boolean = false): void { // Split up raw into a list per folder so we can do a batch add per folder. @@ -1959,8 +1935,14 @@ export class SearchModel extends Disposable { private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; + private readonly _onSearchResultChanged = this._register(new PauseableEmitter({ + merge: mergeSearchResultEvents + })); + readonly onSearchResultChanged: Event = this._onSearchResultChanged.event; + private currentCancelTokenSource: CancellationTokenSource | null = null; private searchCancelledForNewSearch: boolean = false; + private _searchResultChangedListener: IDisposable; constructor( @ISearchService private readonly searchService: ISearchService, @@ -1972,6 +1954,7 @@ export class SearchModel extends Disposable { ) { super(); this._searchResult = this.instantiationService.createInstance(SearchResult, this); + this._searchResultChangedListener = this._register(this._searchResult.onChange((e) => this._onSearchResultChanged.fire(e))); } isReplaceActive(): boolean { @@ -2010,6 +1993,15 @@ export class SearchModel extends Disposable { return this._searchResult; } + set searchResult(searchResult: SearchResult) { + this._searchResult.dispose(); + this._searchResultChangedListener.dispose(); + + this._searchResult = searchResult; + this._searchResult.searchModel = this; + this._searchResultChangedListener = this._register(this._searchResult.onChange((e) => this._onSearchResultChanged.fire(e))); + } + private async doSearch(query: ITextQuery, progressEmitter: Emitter, searchQuery: ITextQuery, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void): Promise { const searchStart = Date.now(); const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource(); @@ -2168,6 +2160,11 @@ export class SearchModel extends Disposable { this.searchResult.dispose(); super.dispose(); } + + transferSearchResult(other: SearchModel): void { + other.searchResult = this._searchResult; + this._searchResult = this.instantiationService.createInstance(SearchResult, this); + } } export type FileMatchOrMatch = FileMatch | Match; @@ -2343,3 +2340,25 @@ function getFileMatches(matches: (FileMatch | FolderMatchWithResource)[]): FileM return fileMatches.concat(folderMatches.map(e => e.allDownstreamFileMatches()).flat()); } + + +function mergeSearchResultEvents(events: IChangeEvent[]): IChangeEvent { + const retEvent: IChangeEvent = { + elements: [], + added: false, + removed: false, + }; + events.forEach((e) => { + if (e.added) { + retEvent.added = true; + } + + if (e.removed) { + retEvent.removed = true; + } + + retEvent.elements = retEvent.elements.concat(e.elements); + }); + + return retEvent; +} diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 2d4afe7a428..cf7506c874c 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -319,6 +319,34 @@ export class SearchView extends ViewPane { } } + private refreshInputs() { + this.pauseSearching = true; + this.searchWidget.setValue(this.viewModel.searchResult.query?.contentPattern.pattern ?? ''); + this.searchWidget.setReplaceAllActionState(false); + this.searchWidget.toggleReplace(true); + this.inputPatternIncludes.setOnlySearchInOpenEditors(this.viewModel.searchResult.query?.onlyOpenEditors || false); + this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(!this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles || true); + this.searchIncludePattern.setValue(''); + this.searchExcludePattern.setValue(''); + this.pauseSearching = false; + } + + public async importSearchResult(searchModel: SearchModel): Promise { + // experimental: used by the quick access search to overwrite a search result + searchModel.transferSearchResult(this.viewModel); + + this.onSearchResultsChanged(); + this.refreshInputs(); + + const collapseResults = this.searchConfig.collapseResults; + if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches().length === 1) { + const onlyMatch = this.viewModel.searchResult.matches()[0]; + if (onlyMatch.count() < 50) { + this.tree.expand(onlyMatch); + } + } + } + protected override renderBody(parent: HTMLElement): void { super.renderBody(parent); this.container = dom.append(parent, dom.$('.search-view')); @@ -431,7 +459,7 @@ export class SearchView extends ViewPane { this.toggleQueryDetails(true, true, true); } - this._register(this.viewModel.searchResult.onChange((event) => this.onSearchResultsChanged(event))); + this._register(this.viewModel.onSearchResultChanged((event) => this.onSearchResultsChanged(event))); this._register(this.onDidChangeBodyVisibility(visible => this.onVisibilityChanged(visible))); @@ -837,7 +865,7 @@ export class SearchView extends ViewPane { this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); const updateHasSomeCollapsible = () => this.toggleCollapseStateDelayer.trigger(() => this.hasSomeCollapsibleResultKey.set(this.hasSomeCollapsible())); updateHasSomeCollapsible(); - this._register(this.viewModel.searchResult.onChange(() => updateHasSomeCollapsible())); + this._register(this.viewModel.onSearchResultChanged(() => updateHasSomeCollapsible())); this._register(this.tree.onDidChangeCollapseState(() => updateHasSomeCollapsible())); this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, DEBOUNCE_DELAY, true)(options => { @@ -1854,7 +1882,7 @@ export class SearchView extends ViewPane { } async open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean, resourceInput?: URI): Promise { - const selection = this.getSelectionFrom(element); + const selection = getEditorSelectionFromMatch(element, this.viewModel); const oldParentMatches = element instanceof Match ? element.parent().matches() : []; const resource = resourceInput ?? (element instanceof Match ? element.parent().resource : (element).resource); let editor: IEditorPane | undefined; @@ -1949,30 +1977,6 @@ export class SearchView extends ViewPane { }, errors.onUnexpectedError); } - private getSelectionFrom(element: FileMatchOrMatch): any { - let match: Match | null = null; - if (element instanceof Match) { - match = element; - } - if (element instanceof FileMatch && element.count() > 0) { - match = element.matches()[element.matches().length - 1]; - } - if (match) { - const range = match.range(); - if (this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) { - const replaceString = match.replaceString; - return { - startLineNumber: range.startLineNumber, - startColumn: range.startColumn, - endLineNumber: range.startLineNumber, - endColumn: range.startColumn + replaceString.length - }; - } - return range; - } - return undefined; - } - private onUntitledDidDispose(resource: URI): void { if (!this.viewModel) { return; @@ -2146,3 +2150,27 @@ class SearchLinkButton extends Disposable { })); } } + +export function getEditorSelectionFromMatch(element: FileMatchOrMatch, viewModel: SearchModel): any { + let match: Match | null = null; + if (element instanceof Match) { + match = element; + } + if (element instanceof FileMatch && element.count() > 0) { + match = element.matches()[element.matches().length - 1]; + } + if (match) { + const range = match.range(); + if (viewModel.isReplaceActive() && !!viewModel.replaceString) { + const replaceString = match.replaceString; + return { + startLineNumber: range.startLineNumber, + startColumn: range.startColumn, + endLineNumber: range.startLineNumber, + endColumn: range.startColumn + replaceString.length + }; + } + return range; + } + return undefined; +} diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index 2b81f549838..d6e4fd9d7a9 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -30,6 +30,7 @@ export const AddCursorsAtSearchResults = 'addCursorsAtSearchResults'; export const RevealInSideBarForSearchResults = 'search.action.revealInSideBar'; export const ReplaceInFilesActionId = 'workbench.action.replaceInFiles'; export const ShowAllSymbolsActionId = 'workbench.action.showAllSymbols'; +export const QuickTextSearchActionId = 'workbench.action.quickTextSearch'; export const CancelSearchActionId = 'search.action.cancel'; export const RefreshSearchResultsActionId = 'search.action.refreshSearchResults'; export const FocusNextSearchResultActionId = 'search.action.focusNextSearchResult'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 7d47ec0a8bf..ddaf7dcb2d5 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -117,7 +117,7 @@ suite('Search Actions', () => { type: QueryType.Text, folderQueries: [{ folder: createFileUriFromPathFromRoot() }], contentPattern: { pattern: '' } - }, searchModel.searchResult, searchModel, null); + }, searchModel.searchResult, searchModel.searchResult, null); return instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index 8f807a2a739..4eae2669c1c 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -205,7 +205,7 @@ suite('searchNotebookHelpers', () => { type: QueryType.Text, folderQueries: [{ folder: createFileUriFromPathFromRoot() }], contentPattern: { pattern: '' } - }, searchModel.searchResult, searchModel, null); + }, searchModel.searchResult, searchModel.searchResult, null); return instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 014e7c143cc..279ee49e361 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -190,7 +190,7 @@ suite('Search - Viewlet', () => { type: QueryType.Text, folderQueries: [{ folder: createFileUriFromPathFromRoot() }], contentPattern: { pattern: '' } - }, parent ?? aSearchResult().folderMatches()[0], searchModel, null); + }, parent ?? aSearchResult().folderMatches()[0], searchModel.searchResult, null); } function aSearchResult(): SearchResult { diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index d20bb9d4370..27bdc71350a 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -408,7 +408,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: TasksQuickAccessProvider.PREFIX, contextKey: tasksPickerContextKey, placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a task to run."), - helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Run Task") }] + helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Run Task"), commandCenterOrder: 60 }] }); // tasks.json validation diff --git a/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts index fcb01ae3e84..7710088cf64 100644 --- a/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts @@ -8,9 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { localize } from 'vs/nls'; import { ICrossVersionSerializedTerminalState, IPtyHostController, ISerializedTerminalState, ITerminalLogService } from 'vs/platform/terminal/common/terminal'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { STATUS_BAR_WARNING_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -68,8 +66,7 @@ export abstract class BaseTerminalBackend extends Disposable { tooltip: localize('nonResponsivePtyHost', "The connection to the terminal's pty host process is unresponsive, terminals may stop working. Click to manually restart the pty host."), ariaLabel: localize('ptyHostStatus.ariaLabel', 'Pty Host is unresponsive'), command: TerminalCommandId.RestartPtyHost, - backgroundColor: themeColorFromId(STATUS_BAR_WARNING_ITEM_BACKGROUND), - color: themeColorFromId(STATUS_BAR_WARNING_ITEM_FOREGROUND), + kind: 'warning' }; } statusBarAccessor = statusBarService.addEntry(unresponsiveStatusBarEntry, 'ptyHostStatus', StatusbarAlignment.LEFT); diff --git a/src/vs/workbench/contrib/terminal/browser/detachedTerminal.ts b/src/vs/workbench/contrib/terminal/browser/detachedTerminal.ts new file mode 100644 index 00000000000..9902a5483e5 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/detachedTerminal.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Delayer } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { OperatingSystem } from 'vs/base/common/platform'; +import { MicrotaskDelay } from 'vs/base/common/symbols'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; +import { ITerminalBackend } from 'vs/platform/terminal/common/terminal'; +import { IDetachedTerminalInstance, IDetachedXTermOptions, IDetachedXtermTerminal, ITerminalContribution, IXtermAttachToElementOptions } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalExtensionsRegistry } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; +import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; +import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { ITerminalProcessInfo, ProcessState } from 'vs/workbench/contrib/terminal/common/terminal'; + +export class DeatachedTerminal extends Disposable implements IDetachedTerminalInstance { + private readonly _widgets = this._register(new TerminalWidgetManager()); + public readonly capabilities = new TerminalCapabilityStore(); + private readonly _contributions: Map = new Map(); + + public get xterm(): IDetachedXtermTerminal { + return this._xterm; + } + + constructor( + private readonly _xterm: XtermTerminal, + options: IDetachedXTermOptions, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._register(_xterm); + + // Initialize contributions + const contributionDescs = TerminalExtensionsRegistry.getTerminalContributions(); + for (const desc of contributionDescs) { + if (this._contributions.has(desc.id)) { + onUnexpectedError(new Error(`Cannot have two terminal contributions with the same id ${desc.id}`)); + continue; + } + if (desc.canRunInDetachedTerminals === false) { + continue; + } + + let contribution: ITerminalContribution; + try { + contribution = instantiationService.createInstance(desc.ctor, this, options.processInfo, this._widgets); + this._contributions.set(desc.id, contribution); + this._register(contribution); + } catch (err) { + onUnexpectedError(err); + } + } + + // xterm is already by the time DetachedTerminal is created, so trigger everything + // on the next microtask, allowing the caller to do any extra initialization + this._register(new Delayer(MicrotaskDelay)).trigger(() => { + for (const contr of this._contributions.values()) { + contr.xtermReady?.(this._xterm); + } + }); + } + + attachToElement(container: HTMLElement, options?: Partial | undefined): void { + const screenElement = this._xterm.attachToElement(container, options); + this._widgets.attachToElement(screenElement); + } +} + +/** + * Implements {@link ITerminalProcessInfo} for a detached terminal where most + * properties are stubbed. Properties are mutable and can be updated by + * the instantiator. + */ +export class DetachedProcessInfo implements ITerminalProcessInfo { + processState = ProcessState.Running; + ptyProcessReady = Promise.resolve(); + shellProcessId: number | undefined; + remoteAuthority: string | undefined; + os: OperatingSystem | undefined; + userHome: string | undefined; + initialCwd = ''; + environmentVariableInfo: IEnvironmentVariableInfo | undefined; + persistentProcessId: number | undefined; + shouldPersist = false; + hasWrittenData = false; + hasChildProcesses = false; + backend: ITerminalBackend | undefined; + capabilities = new TerminalCapabilityStore(); + shellIntegrationNonce = ''; + extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined; + + constructor(initialValues: Partial) { + Object.assign(this, initialValues); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 814626ed4f5..ca3f5ed9a05 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -20,7 +20,7 @@ visibility: hidden; } .monaco-workbench .part.panel .pane-body.integrated-terminal .terminal-outer-container { - background-color: var(--vscode-terminal-background, --vscode-panel-background); + background-color: var(--vscode-terminal-background, var(--vscode-panel-background)); } .monaco-workbench .pane-body.integrated-terminal .terminal-outer-container, .monaco-workbench .pane-body.integrated-terminal .terminal-groups-container, @@ -32,7 +32,7 @@ } .monaco-workbench .part.sidebar .pane-body.integrated-terminal .terminal-outer-container, .monaco-workbench .part.auxiliarybar .pane-body.integrated-terminal .terminal-outer-container { - background-color: var(--vscode-terminal-background, --vscode-sideBar-background); + background-color: var(--vscode-terminal-background, var(--vscode-sideBar-background)); } .monaco-workbench .pane-body.integrated-terminal .split-view-view:not(:first-child), @@ -41,7 +41,7 @@ } .monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay { - background-color: var(--vscode-terminal-dropBackground, --vscode-editorGroup-dropBackground); + background-color: var(--vscode-terminal-dropBackground, var(--vscode-editorGroup-dropBackground)); } .monaco-workbench .pane-body.integrated-terminal .terminal-tabs-entry.is-active::before { @@ -53,6 +53,21 @@ outline: 0 !important; } +.xterm.dev-mode .xterm-helper-textarea { + z-index: 36 !important; + opacity: 1 !important; + left: 0 !important; + width: fit-content !important; +} +.monaco-workbench .xterm.dev-mode .xterm-helper-textarea:focus { + opacity: 1 !important; +} + +.monaco-workbench .xterm .xterm-helper-textarea:focus { + /* Override the general vscode style applies `opacity:1!important` to textareas */ + opacity: 0 !important; +} + .monaco-workbench .terminal-tab::before { font-family: 'codicon' !important; font-size: 16px !important; @@ -63,7 +78,7 @@ } .monaco-workbench .terminal-editor .terminal-wrapper { - background-color: var(--vscode-terminal-background, --vscode-editorPane-background); + background-color: var(--vscode-terminal-background, var(--vscode-editorPane-background)); } .monaco-workbench .terminal-editor .terminal-wrapper, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper { @@ -248,11 +263,6 @@ top: 0; } -.monaco-workbench .xterm .xterm-helper-textarea:focus { - /* Override the general vscode style applies `opacity:1!important` to textareas */ - opacity: 0 !important; -} - .monaco-workbench.vs-dark.mac .pane-body.integrated-terminal .terminal-groups-container:not(.alt-active) .terminal:not(.enable-mouse-events), .monaco-workbench.hc-black.mac .pane-body.integrated-terminal .terminal-groups-container:not(.alt-active) .terminal:not(.enable-mouse-events) { cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text; @@ -474,7 +484,12 @@ } .monaco-workbench .terminal .terminal-command-decoration.quick-fix { color: var(--vscode-editorLightBulb-foreground) !important; - background-color: var(--vscode-terminal-background, --vscode-panel-background); + background-color: var(--vscode-terminal-background, var(--vscode-panel-background)); +} +.monaco-workbench .terminal .terminal-command-decoration.quick-fix.explainOnly { + /* Use success background to blend in with the terminal better as it's lower priority. We will + * probably want to add an explicit color for this eventually. */ + color: var(--vscode-terminalCommandDecoration-successBackground) !important; } .terminal-scroll-highlight { @@ -573,7 +588,7 @@ pointer-events: all; opacity: 1; z-index: 33; - background-color: var(--vscode-terminal-background, --vscode-panel-background); + background-color: var(--vscode-terminal-background, var(--vscode-panel-background)); } .monaco-workbench .xterm.terminal.hide { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index fa324a2b150..eea627b0e88 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -5,9 +5,9 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import 'vs/css!./media/scrollbar'; -import 'vs/css!./media/terminal'; import 'vs/css!./media/widgets'; import 'vs/css!./media/xterm'; +import 'vs/css!./media/terminal'; import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 6d199b0cd13..61b92bd54d9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -22,7 +22,7 @@ import { IEditableData } from 'vs/workbench/common/views'; import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon'; import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; -import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy, ITerminalProcessInfo } from 'vs/workbench/contrib/terminal/common/terminal'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ISimpleSelectedSuggestion } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget'; import type { IMarker, Terminal as RawXtermTerminal } from 'xterm'; @@ -146,8 +146,32 @@ export interface IDetachedXTermOptions { colorProvider: IXtermColorProvider; capabilities?: ITerminalCapabilityStore; readonly?: boolean; + processInfo: ITerminalProcessInfo; } +/** + * A {@link ITerminalInstance}-like object that emulates a subset of + * capabilities. This instance is returned from {@link ITerminalService.createDetachedTerminal} + * to represent terminals that appear in other parts of the VS Code UI outside + * of the "Terminal" view or editors. + */ +export interface IDetachedTerminalInstance extends IDisposable { + readonly xterm: IDetachedXtermTerminal; + readonly capabilities: ITerminalCapabilityStore; + + /** + * Attached the terminal to the given element. This should be preferred over + * calling {@link IXtermTerminal.attachToElement} so that extra DOM elements + * for contributions are initialized. + * + * @param container Container the terminal will be rendered in + * @param options Additional options for mounting the terminal in an element + */ + attachToElement(container: HTMLElement, options?: Partial): void; +} + +export const isDetachedTerminalInstance = (t: ITerminalInstance | IDetachedTerminalInstance): t is IDetachedTerminalInstance => typeof (t as ITerminalInstance).instanceId === 'number'; + export interface ITerminalService extends ITerminalInstanceHost { readonly _serviceBrand: undefined; @@ -192,7 +216,7 @@ export interface ITerminalService extends ITerminalInstanceHost { * tracked as a terminal instance. * @params options The options to create the terminal with */ - createDetachedXterm(options: IDetachedXTermOptions): Promise; + createDetachedTerminal(options: IDetachedXTermOptions): Promise; /** * Creates a raw terminal instance, this should not be used outside of the terminal part. @@ -1107,7 +1131,6 @@ export interface IXtermTerminal extends IDisposable { } export interface IDetachedXtermTerminal extends IXtermTerminal { - /** * Writes data to the terminal. * @param data data to write diff --git a/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts b/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts index a9e85fa6eb4..6657524b166 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts @@ -5,19 +5,24 @@ import { BrandedService, IConstructorSignature } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ITerminalContribution, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProcessInfo, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +/** Constructor compatible with full terminal instances, is assignable to {@link DetachedCompatibleTerminalContributionCtor} */ export type TerminalContributionCtor = IConstructorSignature; +/** Constructor compatible with detached terminals */ +export type DetachedCompatibleTerminalContributionCtor = IConstructorSignature; -export interface ITerminalContributionDescription { - readonly id: string; - readonly ctor: TerminalContributionCtor; -} +export type ITerminalContributionDescription = { readonly id: string } & ( + | { readonly canRunInDetachedTerminals: false; readonly ctor: TerminalContributionCtor } + | { readonly canRunInDetachedTerminals: true; readonly ctor: DetachedCompatibleTerminalContributionCtor } +); -export function registerTerminalContribution(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }): void { - TerminalContributionRegistry.INSTANCE.registerTerminalContribution(id, ctor); +export function registerTerminalContribution(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals?: false): void; +export function registerTerminalContribution(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessInfo, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals: true): void; +export function registerTerminalContribution(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals = false): void { + TerminalContributionRegistry.INSTANCE.registerTerminalContribution({ id, ctor, canRunInDetachedTerminals } as ITerminalContributionDescription); } export namespace TerminalExtensionsRegistry { @@ -35,8 +40,8 @@ class TerminalContributionRegistry { constructor() { } - public registerTerminalContribution(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }): void { - this._terminalContributions.push({ id, ctor: ctor as TerminalContributionCtor }); + public registerTerminalContribution(description: ITerminalContributionDescription): void { + this._terminalContributions.push(description); } public getTerminalContributions(): ITerminalContributionDescription[] { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 23b4cbdfd3d..2423c3958b1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -29,7 +29,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { VirtualWorkspaceContext } from 'vs/workbench/common/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IDetachedXTermOptions, IDetachedXtermTerminal, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, IXtermTerminal, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, IDetachedTerminalInstance, IDetachedXTermOptions, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, IXtermTerminal, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { getCwdForSplit } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; @@ -52,6 +52,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; import { mark } from 'vs/base/common/performance'; +import { DeatachedTerminal } from 'vs/workbench/contrib/terminal/browser/detachedTerminal'; export class TerminalService extends Disposable implements ITerminalService { declare _serviceBrand: undefined; @@ -1018,9 +1019,9 @@ export class TerminalService extends Disposable implements ITerminalService { return this._createTerminal(shellLaunchConfig, location, options); } - async createDetachedXterm(options: IDetachedXTermOptions): Promise { + async createDetachedTerminal(options: IDetachedXTermOptions): Promise { const ctor = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService); - const instance = this._instantiationService.createInstance( + const xterm = this._instantiationService.createInstance( XtermTerminal, ctor, this._configHelper, @@ -1034,13 +1035,16 @@ export class TerminalService extends Disposable implements ITerminalService { ); if (options.readonly) { - instance.raw.attachCustomKeyEventHandler(() => false); + xterm.raw.attachCustomKeyEventHandler(() => false); } - this._detachedXterms.add(instance); - instance.onDidDispose(() => this._detachedXterms.delete(instance)); + this._detachedXterms.add(xterm); + const l = xterm.onDidDispose(() => { + this._detachedXterms.delete(xterm); + l.dispose(); + }); - return instance; + return new DeatachedTerminal(xterm, options, this._instantiationService); } private async _resolveCwd(shellLaunchConfig: IShellLaunchConfig, splitActiveTerminal: boolean, options?: ICreateTerminalOptions): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts index 46ee2d10d7a..4d475759e0b 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts @@ -29,8 +29,7 @@ export const enum DecorationSelector { Codicon = 'codicon', XtermDecoration = 'xterm-decoration', OverviewRuler = '.xterm-decoration-overview-ruler', - QuickFix = 'quick-fix', - LightBulb = 'codicon-light-bulb' + QuickFix = 'quick-fix' } export class TerminalDecorationHoverManager extends Disposable { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 1aa541b9a56..fcf7a078831 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -201,7 +201,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID @IThemeService private readonly _themeService: IThemeService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IClipboardService private readonly _clipboardService: IClipboardService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); const font = this._configHelper.getFont(undefined, true); @@ -961,7 +961,7 @@ export function getXtermScaledDimensions(font: ITerminalFont, width: number, hei function vscodeToXtermLogLevel(logLevel: LogLevel): XtermLogLevel { switch (logLevel) { - case LogLevel.Trace: + case LogLevel.Trace: return 'trace'; case LogLevel.Debug: return 'debug'; case LogLevel.Info: return 'info'; case LogLevel.Warning: return 'warn'; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 3f528b3908c..77e37c5b574 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -253,7 +253,8 @@ export interface IDefaultShellAndArgsRequest { callback: (shell: string, args: string[] | string | undefined) => void; } -export interface ITerminalProcessManager extends IDisposable { +/** Read-only process information that can apply to detached terminals. */ +export interface ITerminalProcessInfo { readonly processState: ProcessState; readonly ptyProcessReady: Promise; readonly shellProcessId: number | undefined; @@ -270,7 +271,11 @@ export interface ITerminalProcessManager extends IDisposable { readonly capabilities: ITerminalCapabilityStore; readonly shellIntegrationNonce: string; readonly extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined; +} +export const isTerminalProcessManager = (t: ITerminalProcessInfo | ITerminalProcessManager): t is ITerminalProcessManager => typeof (t as ITerminalProcessManager).write === 'function'; + +export interface ITerminalProcessManager extends IDisposable, ITerminalProcessInfo { readonly onPtyDisconnect: Event; readonly onPtyReconnect: Event; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index cc697917066..b69943c18de 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -22,8 +22,31 @@ import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { TerminalAccessibleContentProvider } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp'; import { AccessibleBufferWidget, NavigationType } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer'; +import { TextAreaSyncAddon } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon'; import type { Terminal } from 'xterm'; + +class TextAreaSyncContribution extends DisposableStore implements ITerminalContribution { + static readonly ID = 'terminal.textAreaSync'; + static get(instance: ITerminalInstance): TextAreaSyncContribution | null { + return instance.getContribution(TextAreaSyncContribution.ID); + } + constructor( + private readonly _instance: ITerminalInstance, + processManager: ITerminalProcessManager, + widgetManager: TerminalWidgetManager, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + } + xtermReady(xterm: IXtermTerminal & { raw: Terminal }): void { + const addon = this._instantiationService.createInstance(TextAreaSyncAddon, this._instance.capabilities); + xterm.raw.loadAddon(addon); + addon.activate(xterm.raw); + } +} +registerTerminalContribution(TextAreaSyncContribution.ID, TextAreaSyncContribution); + class AccessibleBufferContribution extends DisposableStore implements ITerminalContribution { static readonly ID = 'terminal.accessible-buffer'; private _xterm: IXtermTerminal & { raw: Terminal } | undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts index 58bd11c6150..d3a0e3a592c 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts @@ -38,7 +38,6 @@ export class TerminalAccessibleContentProvider extends Disposable implements IAc } options: IAccessibleViewOptions = { type: AccessibleViewType.Help, - ariaLabel: localize('terminal-help-label', "terminal accessibility help"), readMoreUrl: 'https://code.visualstudio.com/docs/editor/accessibility#_terminal-accessibility' }; verbositySettingKey = AccessibilityVerbositySettingId.Terminal; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon.ts new file mode 100644 index 00000000000..666f27389d9 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; +import type { Terminal, ITerminalAddon } from 'xterm'; +import { debounce } from 'vs/base/common/decorators'; +import { addDisposableListener } from 'vs/base/browser/dom'; + +export interface ITextAreaData { + content: string; + cursorX: number; +} + +export class TextAreaSyncAddon extends Disposable implements ITerminalAddon { + private _terminal: Terminal | undefined; + private _listeners = this._register(new MutableDisposable()); + private _currentCommand: string | undefined; + private _cursorX: number | undefined; + + activate(terminal: Terminal): void { + this._terminal = terminal; + if (this._accessibilityService.isScreenReaderOptimized()) { + this._registerSyncListeners(); + } + } + + constructor( + private readonly _capabilities: ITerminalCapabilityStore, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @ITerminalLogService private readonly _logService: ITerminalLogService + ) { + super(); + this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => { + if (this._accessibilityService.isScreenReaderOptimized()) { + this._syncTextArea(); + this._registerSyncListeners(); + } else { + this._listeners.clear(); + } + })); + } + + private _registerSyncListeners(): void { + if (this._accessibilityService.isScreenReaderOptimized() && this._terminal?.textarea) { + this._listeners.value = new DisposableStore(); + this._listeners.value.add(this._terminal.onCursorMove(() => this._syncTextArea())); + this._listeners.value.add(this._terminal.onData(() => this._syncTextArea())); + this._listeners.value.add(addDisposableListener(this._terminal.textarea, 'focus', () => this._syncTextArea())); + } + } + + @debounce(50) + private _syncTextArea(): void { + this._logService.debug('TextAreaSyncAddon#syncTextArea'); + const textArea = this._terminal?.textarea; + if (!textArea) { + this._logService.debug(`TextAreaSyncAddon#syncTextArea: no textarea`); + return; + } + + this._updateCommandAndCursor(); + + if (this._currentCommand !== textArea.value) { + textArea.value = this._currentCommand || ''; + this._logService.debug(`TextAreaSyncAddon#syncTextArea: text changed to "${this._currentCommand}"`); + } else if (!this._currentCommand) { + textArea.value = ''; + this._logService.debug(`TextAreaSyncAddon#syncTextArea: text cleared`); + } + + if (this._cursorX !== textArea.selectionStart) { + textArea.selectionStart = this._cursorX ?? 0; + textArea.selectionEnd = this._cursorX ?? 0; + this._logService.debug(`TextAreaSyncAddon#syncTextArea: selection start/end changed to ${this._cursorX}`); + } + } + + private _updateCommandAndCursor(): void { + if (!this._terminal) { + return; + } + const commandCapability = this._capabilities.get(TerminalCapability.CommandDetection); + const currentCommand = commandCapability?.currentCommand; + if (!currentCommand) { + this._logService.debug(`TextAreaSyncAddon#updateCommandAndCursor: no current command`); + return; + } + const buffer = this._terminal.buffer.active; + const line = buffer.getLine(buffer.cursorY)?.translateToString(true); + if (!line) { + this._logService.debug(`TextAreaSyncAddon#updateCommandAndCursor: no line`); + return; + } + if (currentCommand.commandStartX !== undefined) { + // Left prompt + this._currentCommand = line.substring(currentCommand.commandStartX); + this._cursorX = buffer.cursorX - currentCommand.commandStartX; + } else if (currentCommand.commandRightPromptStartX !== undefined) { + // Right prompt + this._currentCommand = line.substring(0, currentCommand.commandRightPromptStartX); + this._cursorX = buffer.cursorX; + } else { + this._currentCommand = undefined; + this._cursorX = undefined; + this._logService.debug(`TextAreaSyncAddon#updateCommandAndCursor: neither commandStartX nor commandRightPromptStartX`); + } + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts b/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts index 147b8676571..4d828a105ce 100644 --- a/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts @@ -4,19 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; +import { ITerminalLogService, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IInternalXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IInternalXtermTerminal, ITerminalContribution, ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; -import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; +import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import type { Terminal } from 'xterm'; registerTerminalAction({ id: TerminalCommandId.ShowTextureAtlas, @@ -102,3 +107,33 @@ registerTerminalAction({ } } }); + +class DevModeContribution extends DisposableStore implements ITerminalContribution { + static readonly ID = 'terminal.devMode'; + private _xterm: IXtermTerminal & { raw: Terminal } | undefined; + static get(instance: ITerminalInstance): DevModeContribution | null { + return instance.getContribution(DevModeContribution.ID); + } + constructor( + instance: ITerminalInstance, + processManager: ITerminalProcessManager, + widgetManager: TerminalWidgetManager, + @IConfigurationService private readonly _configurationService: IConfigurationService) { + super(); + this.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalSettingId.DevMode)) { + this._updateDevMode(); + } + })); + } + xtermReady(xterm: IXtermTerminal & { raw: Terminal }): void { + this._xterm = xterm; + this._updateDevMode(); + } + + private _updateDevMode() { + const devMode: boolean = this._configurationService.getValue(TerminalSettingId.DevMode) || false; + this._xterm?.raw.element?.classList.toggle('dev-mode', devMode); + } +} +registerTerminalContribution(DevModeContribution.ID, DevModeContribution); diff --git a/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts b/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts index ed59231bb57..e4bcaf9b0c0 100644 --- a/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts @@ -55,15 +55,14 @@ function describeEnvironmentChanges(collection: IMergedEnvironmentVariableCollec content += '\n'; const globalDescription = globalDescriptions.get(ext); if (globalDescription) { - content += `\n${globalDescription}`; + content += `\n${globalDescription}\n`; } const workspaceDescription = workspaceDescriptions.get(ext); if (workspaceDescription) { // Only show '(workspace)' suffix if there is already a description for the extension. const workspaceSuffix = globalDescription ? ` (${localize('ScopedEnvironmentContributionInfo', 'workspace')})` : ''; - content += `\n${workspaceDescription}${workspaceSuffix}`; + content += `\n${workspaceDescription}${workspaceSuffix}\n`; } - content += '\n'; for (const mutator of coll.map.values()) { if (filterScope(mutator, scope) === false) { diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts index 780540bc02b..efb9b621568 100644 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts @@ -80,17 +80,18 @@ export class TerminalFindWidget extends SimpleFindWidget { override reveal(): void { const initialInput = this._instance.hasSelection() && !this._instance.selection!.includes('\n') ? this._instance.selection : undefined; + const inputValue = initialInput ?? this.inputValue; const xterm = this._instance.xterm; - if (xterm && this.inputValue && this.inputValue !== '') { + if (xterm && inputValue && inputValue !== '') { // trigger highlight all matches - this._findPreviousWithEvent(xterm, this.inputValue, { incremental: true, regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }).then(foundMatch => { + this._findPreviousWithEvent(xterm, inputValue, { incremental: true, regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }).then(foundMatch => { this.updateButtons(foundMatch); this._register(Event.once(xterm.onDidChangeSelection)(() => xterm.clearActiveSearchDecoration())); }); } this.updateButtons(false); - super.reveal(initialInput); + super.reveal(inputValue); this._findWidgetVisible.set(true); } diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts index bfc8d964116..5d144353e9f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts @@ -10,11 +10,11 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveInstanceAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProcessInfo, ITerminalProcessManager, TerminalCommandId, isTerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { ITerminalLinkProviderService } from 'vs/workbench/contrib/terminalContrib/links/browser/links'; @@ -40,8 +40,8 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr private _linkResolver: TerminalLinkResolver; constructor( - private readonly _instance: ITerminalInstance, - private readonly _processManager: ITerminalProcessManager, + private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, + private readonly _processManager: ITerminalProcessManager | ITerminalProcessInfo, private readonly _widgetManager: TerminalWidgetManager, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITerminalLinkProviderService private readonly _terminalLinkProviderService: ITerminalLinkProviderService @@ -52,18 +52,24 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { const linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm.raw, this._processManager, this._instance.capabilities, this._linkResolver); - this._processManager.onProcessReady(() => { + if (isTerminalProcessManager(this._processManager)) { + this._processManager.onProcessReady(() => { + linkManager.setWidgetManager(this._widgetManager); + }); + } else { linkManager.setWidgetManager(this._widgetManager); - }); + } this._linkManager = this.add(linkManager); // Attach the link provider(s) to the instance and listen for changes - for (const linkProvider of this._terminalLinkProviderService.linkProviders) { - this._linkManager.registerExternalLinkProvider(linkProvider.provideLinks.bind(linkProvider, this._instance)); + if (!isDetachedTerminalInstance(this._instance)) { + for (const linkProvider of this._terminalLinkProviderService.linkProviders) { + this._linkManager.registerExternalLinkProvider(linkProvider.provideLinks.bind(linkProvider, this._instance)); + } + this.add(this._terminalLinkProviderService.onDidAddLinkProvider(e => { + linkManager.registerExternalLinkProvider(e.provideLinks.bind(e, this._instance as ITerminalInstance)); + })); } - this.add(this._terminalLinkProviderService.onDidAddLinkProvider(e => { - linkManager.registerExternalLinkProvider(e.provideLinks.bind(e, this._instance)); - })); // TODO: Currently only a single link provider is supported; the one registered by the ext host this.add(this._terminalLinkProviderService.onDidRemoveLinkProvider(e => { linkManager.dispose(); @@ -97,7 +103,7 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr } } -registerTerminalContribution(TerminalLinkContribution.ID, TerminalLinkContribution); +registerTerminalContribution(TerminalLinkContribution.ID, TerminalLinkContribution, true); const category = terminalStrings.actionCategory; diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 3cce34e79b6..93105d6e3c0 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -25,7 +25,7 @@ import { ILinkHoverTargetOptions, TerminalHover } from 'vs/workbench/contrib/ter import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { ITerminalConfiguration, ITerminalProcessManager, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalConfiguration, ITerminalProcessInfo, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; import { IHoverAction } from 'vs/workbench/services/hover/browser/hover'; import type { ILink, ILinkProvider, IViewportRange, Terminal } from 'xterm'; import { convertBufferRangeToViewport } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers'; @@ -47,7 +47,7 @@ export class TerminalLinkManager extends DisposableStore { constructor( private readonly _xterm: Terminal, - private readonly _processManager: ITerminalProcessManager, + private readonly _processInfo: ITerminalProcessInfo, capabilities: ITerminalCapabilityStore, private readonly _linkResolver: ITerminalLinkResolver, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -65,15 +65,15 @@ export class TerminalLinkManager extends DisposableStore { enableFileLinks = false; break; case 'notRemote': - enableFileLinks = !this._processManager.remoteAuthority; + enableFileLinks = !this._processInfo.remoteAuthority; break; } // Setup link detectors in their order of priority - this._setupLinkDetector(TerminalUriLinkDetector.id, this._instantiationService.createInstance(TerminalUriLinkDetector, this._xterm, this._processManager, this._linkResolver)); + this._setupLinkDetector(TerminalUriLinkDetector.id, this._instantiationService.createInstance(TerminalUriLinkDetector, this._xterm, this._processInfo, this._linkResolver)); if (enableFileLinks) { - this._setupLinkDetector(TerminalMultiLineLinkDetector.id, this._instantiationService.createInstance(TerminalMultiLineLinkDetector, this._xterm, this._processManager, this._linkResolver)); - this._setupLinkDetector(TerminalLocalLinkDetector.id, this._instantiationService.createInstance(TerminalLocalLinkDetector, this._xterm, capabilities, this._processManager, this._linkResolver)); + this._setupLinkDetector(TerminalMultiLineLinkDetector.id, this._instantiationService.createInstance(TerminalMultiLineLinkDetector, this._xterm, this._processInfo, this._linkResolver)); + this._setupLinkDetector(TerminalLocalLinkDetector.id, this._instantiationService.createInstance(TerminalLocalLinkDetector, this._xterm, capabilities, this._processInfo, this._linkResolver)); } this._setupLinkDetector(TerminalWordLinkDetector.id, this.add(this._instantiationService.createInstance(TerminalWordLinkDetector, this._xterm))); @@ -83,8 +83,8 @@ export class TerminalLinkManager extends DisposableStore { this._openers.set(TerminalBuiltinLinkType.LocalFile, localFileOpener); this._openers.set(TerminalBuiltinLinkType.LocalFolderInWorkspace, localFolderInWorkspaceOpener); this._openers.set(TerminalBuiltinLinkType.LocalFolderOutsideWorkspace, this._instantiationService.createInstance(TerminalLocalFolderOutsideWorkspaceLinkOpener)); - this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, this._processManager.initialCwd, localFileOpener, localFolderInWorkspaceOpener, () => this._processManager.os || OS)); - this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processManager.remoteAuthority)); + this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, this._processInfo.initialCwd, localFileOpener, localFolderInWorkspaceOpener, () => this._processInfo.os || OS)); + this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processInfo.remoteAuthority)); this._registerStandardLinkProviders(); diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix.ts index 382d5144b23..f576102fe85 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix.ts @@ -29,7 +29,7 @@ export interface ITerminalQuickFixProviderSelector { provider: ITerminalQuickFixProvider; } -export type TerminalQuickFixActionInternal = IAction | ITerminalQuickFixCommandAction | ITerminalQuickFixOpenerAction; +export type TerminalQuickFixActionInternal = IAction | ITerminalQuickFixExecuteTerminalCommandAction | ITerminalQuickFixOpenerAction; export type TerminalQuickFixCallback = (matchResult: ITerminalCommandMatchResult) => TerminalQuickFixActionInternal[] | TerminalQuickFixActionInternal | undefined; export type TerminalQuickFixCallbackExtension = (terminalCommand: ITerminalCommand, lines: string[] | undefined, option: ITerminalQuickFixOptions, token: CancellationToken) => Promise; @@ -44,9 +44,10 @@ export interface ITerminalQuickFixProvider { } export enum TerminalQuickFixType { - Command = 0, + TerminalCommand = 0, Opener = 1, - Port = 2 + Port = 2, + VscodeCommand = 3 } export interface ITerminalQuickFixOptions { @@ -55,6 +56,7 @@ export interface ITerminalQuickFixOptions { commandLineMatcher: string | RegExp; outputMatcher?: ITerminalOutputMatcher; commandExitResult: 'success' | 'error'; + kind?: 'fix' | 'explain'; } export interface ITerminalQuickFix { @@ -63,8 +65,8 @@ export interface ITerminalQuickFix { source: string; } -export interface ITerminalQuickFixCommandAction extends ITerminalQuickFix { - type: TerminalQuickFixType.Command; +export interface ITerminalQuickFixExecuteTerminalCommandAction extends ITerminalQuickFix { + type: TerminalQuickFixType.TerminalCommand; terminalCommand: string; // TODO: Should this depend on whether alt is held? addNewLine?: boolean; @@ -73,6 +75,9 @@ export interface ITerminalQuickFixOpenerAction extends ITerminalQuickFix { type: TerminalQuickFixType.Opener; uri: URI; } +export interface ITerminalQuickFixCommandAction extends ITerminalQuickFix { + title: string; +} export interface ITerminalCommandMatchResult { commandLine: string; diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts index 765d03c2141..9f1ccc65e8c 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts @@ -28,16 +28,16 @@ import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { ILabelService } from 'vs/platform/label/common/label'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; -import { ITerminalQuickFixInternalOptions, ITerminalQuickFixResolvedExtensionOptions, ITerminalQuickFix, ITerminalQuickFixCommandAction, ITerminalQuickFixOpenerAction, ITerminalQuickFixOptions, ITerminalQuickFixProviderSelector, ITerminalQuickFixService, ITerminalQuickFixUnresolvedExtensionOptions, TerminalQuickFixType } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix'; +import { ITerminalQuickFixInternalOptions, ITerminalQuickFixResolvedExtensionOptions, ITerminalQuickFix, ITerminalQuickFixExecuteTerminalCommandAction, ITerminalQuickFixOpenerAction, ITerminalQuickFixOptions, ITerminalQuickFixProviderSelector, ITerminalQuickFixService, ITerminalQuickFixUnresolvedExtensionOptions, TerminalQuickFixType, ITerminalQuickFixCommandAction } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix'; import { ITerminalCommandSelector } from 'vs/platform/terminal/common/terminal'; import { ActionListItemKind, IActionListItem } from 'vs/platform/actionWidget/browser/actionList'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; +import { ICommandService } from 'vs/platform/commands/common/commands'; -const quickFixSelectors = [ +const quickFixClasses = [ DecorationSelector.QuickFix, - DecorationSelector.LightBulb, DecorationSelector.Codicon, DecorationSelector.CommandDecoration, DecorationSelector.XtermDecoration @@ -75,6 +75,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, private readonly _aliases: string[][] | undefined, private readonly _capabilities: ITerminalCapabilityStore, @ITerminalQuickFixService private readonly _quickFixService: ITerminalQuickFixService, + @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IAudioCueService private readonly _audioCueService: IAudioCueService, @IOpenerService private readonly _openerService: IOpenerService, @@ -114,7 +115,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, } // TODO: What's documentation do? Need a vscode command? - const actions = this._currentRenderContext.quickFixes.map(f => new TerminalQuickFixItem(f, f.type, f.source, f.label)); + const actions = this._currentRenderContext.quickFixes.map(f => new TerminalQuickFixItem(f, f.type, f.source, f.label, f.kind)); const documentation = this._currentRenderContext.quickFixes.map(f => { return { id: f.source, title: f.label, tooltip: f.source }; }); const actionSet = { // TODO: Documentation and actions are separate? @@ -148,7 +149,8 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, type: 'unresolved', commandLineMatcher: selector.commandLineMatcher, outputMatcher: selector.outputMatcher, - commandExitResult: selector.commandExitResult + commandExitResult: selector.commandExitResult, + kind: selector.kind }); this._registeredSelectors.add(selector.id); this._commandListeners.set(matcherKey, currentOptions); @@ -190,9 +192,16 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, } const id = selector.id; await this._extensionService.activateByEvent(`onTerminalQuickFixRequest:${id}`); - return this._quickFixService.providers.get(id)?.provideTerminalQuickFixes(command, lines, { type: 'resolved', commandLineMatcher: selector.commandLineMatcher, outputMatcher: selector.outputMatcher, commandExitResult: selector.commandExitResult, id: selector.id }, new CancellationTokenSource().token); + return this._quickFixService.providers.get(id)?.provideTerminalQuickFixes(command, lines, { + type: 'resolved', + commandLineMatcher: selector.commandLineMatcher, + outputMatcher: selector.outputMatcher, + commandExitResult: selector.commandExitResult, + kind: selector.kind, + id: selector.id + }, new CancellationTokenSource().token); }; - const result = await getQuickFixesForCommand(aliases, terminal, command, this._commandListeners, this._openerService, this._labelService, this._onDidRequestRerunCommand, resolver); + const result = await getQuickFixesForCommand(aliases, terminal, command, this._commandListeners, this._commandService, this._openerService, this._labelService, this._onDidRequestRerunCommand, resolver); if (!result) { return; } @@ -264,7 +273,13 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, return; } - e.classList.add(...quickFixSelectors); + e.classList.add(...quickFixClasses); + const isExplainOnly = fixes.every(e => e.kind === 'explain'); + if (isExplainOnly) { + e.classList.add('explainOnly'); + } + e.classList.add(...ThemeIcon.asClassNameArray(isExplainOnly ? Codicon.sparkle : Codicon.lightBulb)); + updateLayout(this._configurationService, e); this._audioCueService.playAudioCue(AudioCue.terminalQuickFix); @@ -283,6 +298,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, export interface ITerminalAction extends IAction { type: TerminalQuickFixType; + kind?: 'fix' | 'explain'; source: string; uri?: URI; command?: string; @@ -293,6 +309,7 @@ export async function getQuickFixesForCommand( terminal: Terminal, terminalCommand: ITerminalCommand, quickFixOptions: Map, + commandService: ICommandService, openerService: IOpenerService, labelService: ILabelService, onDidRequestRerunCommand?: Emitter<{ command: string; addNewLine?: boolean }>, @@ -339,15 +356,16 @@ export async function getQuickFixesForCommand( let action: ITerminalAction | undefined; if ('type' in quickFix) { switch (quickFix.type) { - case TerminalQuickFixType.Command: { - const fix = quickFix as ITerminalQuickFixCommandAction; + case TerminalQuickFixType.TerminalCommand: { + const fix = quickFix as ITerminalQuickFixExecuteTerminalCommandAction; if (commandQuickFixSet.has(fix.terminalCommand)) { continue; } commandQuickFixSet.add(fix.terminalCommand); const label = localize('quickFix.command', 'Run: {0}', fix.terminalCommand); action = { - type: TerminalQuickFixType.Command, + type: TerminalQuickFixType.TerminalCommand, + kind: option.kind, class: undefined, source: quickFix.source, id: quickFix.id, @@ -381,6 +399,7 @@ export async function getQuickFixesForCommand( id: quickFix.id, label, type: TerminalQuickFixType.Opener, + kind: option.kind, class: undefined, enabled: true, run: () => openerService.open(fix.uri), @@ -394,6 +413,7 @@ export async function getQuickFixesForCommand( action = { source: 'builtin', type: fix.type, + kind: option.kind, id: fix.id, label: fix.label, class: fix.class, @@ -405,6 +425,21 @@ export async function getQuickFixesForCommand( }; break; } + case TerminalQuickFixType.VscodeCommand: { + const fix = quickFix as ITerminalQuickFixCommandAction; + action = { + source: quickFix.source, + type: fix.type, + kind: option.kind, + id: fix.id, + label: fix.title, + class: undefined, + enabled: true, + run: () => commandService.executeCommand(fix.id), + tooltip: fix.title + }; + break; + } } if (action) { fixes.push(action); @@ -424,22 +459,20 @@ function convertToQuickFixOptions(selectorProvider: ITerminalQuickFixProviderSel commandLineMatcher: selectorProvider.selector.commandLineMatcher, outputMatcher: selectorProvider.selector.outputMatcher, commandExitResult: selectorProvider.selector.commandExitResult, + kind: selectorProvider.selector.kind, getQuickFixes: selectorProvider.provider.provideTerminalQuickFixes }; } class TerminalQuickFixItem { - action: ITerminalAction; - type: TerminalQuickFixType; - disabled?: boolean; - title?: string; - source: string; - constructor(action: ITerminalAction, type: TerminalQuickFixType, source: string, title?: string, disabled?: boolean) { - this.action = action; - this.disabled = disabled; - this.title = title; - this.source = source; - this.type = type; + readonly disabled = false; + constructor( + readonly action: ITerminalAction, + readonly type: TerminalQuickFixType, + readonly source: string, + readonly title: string | undefined, + readonly kind: 'fix' | 'explain' = 'fix' + ) { } } @@ -471,15 +504,20 @@ function toActionWidgetItems(inputQuickFixes: readonly TerminalQuickFixItem[], s } function getQuickFixIcon(quickFix: TerminalQuickFixItem): ThemeIcon { + if (quickFix.kind === 'explain') { + return Codicon.sparkle; + } switch (quickFix.type) { case TerminalQuickFixType.Opener: if ('uri' in quickFix.action && quickFix.action.uri) { const isUrl = (quickFix.action.uri.scheme === Schemas.http || quickFix.action.uri.scheme === Schemas.https); return isUrl ? Codicon.linkExternal : Codicon.goToFile; } - case TerminalQuickFixType.Command: + case TerminalQuickFixType.TerminalCommand: return Codicon.run; case TerminalQuickFixType.Port: return Codicon.debugDisconnect; + case TerminalQuickFixType.VscodeCommand: + return Codicon.lightbulb; } } diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts index f910bb1b7c3..96f2fccd74f 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminal.quickFix.contribution.ts @@ -55,7 +55,7 @@ class TerminalQuickFixContribution extends DisposableStore implements ITerminalC // Register quick fixes for (const actionOption of [ gitTwoDashes(), - freePort(this), + freePort((port: string, command: string) => this._instance.freePortKillProcess(port, command)), gitSimilar(), gitPushSetUpstream(), gitCreatePr(), diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixBuiltinActions.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixBuiltinActions.ts index 0bc357b08b4..89284446f73 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixBuiltinActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixBuiltinActions.ts @@ -5,8 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { ITerminalQuickFixInternalOptions, ITerminalCommandMatchResult, ITerminalQuickFixCommandAction, TerminalQuickFixActionInternal, TerminalQuickFixType } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix'; +import { ITerminalQuickFixInternalOptions, ITerminalCommandMatchResult, ITerminalQuickFixExecuteTerminalCommandAction, TerminalQuickFixActionInternal, TerminalQuickFixType } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix'; export const GitCommandLineRegex = /git/; export const GitPushCommandLineRegex = /git\s+push/; @@ -49,7 +48,7 @@ export function gitSimilar(): ITerminalQuickFixInternalOptions { if (fixedCommand) { actions.push({ id: 'Git Similar', - type: TerminalQuickFixType.Command, + type: TerminalQuickFixType.TerminalCommand, terminalCommand: matchResult.commandLine.replace(/git\s+[^\s]+/, () => `git ${fixedCommand}`), addNewLine: true, source: QuickFixSource.Builtin @@ -79,7 +78,7 @@ export function gitTwoDashes(): ITerminalQuickFixInternalOptions { return; } return { - type: TerminalQuickFixType.Command, + type: TerminalQuickFixType.TerminalCommand, id: 'Git Two Dashes', terminalCommand: matchResult.commandLine.replace(` -${problemArg}`, () => ` --${problemArg}`), addNewLine: true, @@ -88,7 +87,7 @@ export function gitTwoDashes(): ITerminalQuickFixInternalOptions { } }; } -export function freePort(terminalInstance?: Partial): ITerminalQuickFixInternalOptions { +export function freePort(runCallback: (port: string, commandLine: string) => Promise): ITerminalQuickFixInternalOptions { return { id: 'Free Port', type: 'internal', @@ -114,9 +113,7 @@ export function freePort(terminalInstance?: Partial): ITermin label, enabled: true, source: QuickFixSource.Builtin, - run: async () => { - await terminalInstance?.freePortKillProcess?.(port, matchResult.commandLine); - } + run: () => runCallback(port, matchResult.commandLine) }; } }; @@ -175,7 +172,7 @@ export function gitPushSetUpstream(): ITerminalQuickFixInternalOptions { } if (fixedCommand) { actions.push({ - type: TerminalQuickFixType.Command, + type: TerminalQuickFixType.TerminalCommand, id: 'Git Push Set Upstream', terminalCommand: fixedCommand, addNewLine: true, @@ -268,11 +265,11 @@ export function pwshGeneralError(): ITerminalQuickFixInternalOptions { if (!suggestions) { return; } - const result: ITerminalQuickFixCommandAction[] = []; + const result: ITerminalQuickFixExecuteTerminalCommandAction[] = []; for (const suggestion of suggestions) { result.push({ id: 'Pwsh General Error', - type: TerminalQuickFixType.Command, + type: TerminalQuickFixType.TerminalCommand, terminalCommand: suggestion, source: QuickFixSource.Builtin }); @@ -314,7 +311,7 @@ export function pwshUnixCommandNotFoundError(): ITerminalQuickFixInternalOptions } // Always remove the first element as it's the "Suggestion [cmd-not-found]"" line - const result: ITerminalQuickFixCommandAction[] = []; + const result: ITerminalQuickFixExecuteTerminalCommandAction[] = []; let inSuggestions = false; for (; i < lines.length; i++) { const line = lines[i].trim(); @@ -325,7 +322,7 @@ export function pwshUnixCommandNotFoundError(): ITerminalQuickFixInternalOptions if (installCommand) { result.push({ id: 'Pwsh Unix Command Not Found Error', - type: TerminalQuickFixType.Command, + type: TerminalQuickFixType.TerminalCommand, terminalCommand: installCommand, source: QuickFixSource.Builtin }); @@ -339,7 +336,7 @@ export function pwshUnixCommandNotFoundError(): ITerminalQuickFixInternalOptions if (inSuggestions) { result.push({ id: 'Pwsh Unix Command Not Found Error', - type: TerminalQuickFixType.Command, + type: TerminalQuickFixType.TerminalCommand, terminalCommand: line.trim(), source: QuickFixSource.Builtin }); diff --git a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixService.ts b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixService.ts index 4119c99c047..61b17085bb6 100644 --- a/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixService.ts +++ b/src/vs/workbench/contrib/terminalContrib/quickFix/browser/terminalQuickFixService.ts @@ -139,6 +139,14 @@ const quickFixExtensionPoint = ExtensionsRegistry.registerExtensionPoint { let quickFixAddon: TerminalQuickFixAddon; - let terminalInstance: Pick; let commandDetection: CommandDetectionCapability; + let commandService: TestCommandService; let openerService: OpenerService; let labelService: LabelService; let terminal: Terminal; @@ -62,9 +62,7 @@ suite('QuickFixAddon', () => { capabilities.add(TerminalCapability.CommandDetection, commandDetection); instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); instantiationService.stub(IOpenerService, {} as Partial); - terminalInstance = { - async freePortKillProcess(port: string): Promise { } - } as Pick; + commandService = new TestCommandService(instantiationService); quickFixAddon = instantiationService.createInstance(TerminalQuickFixAddon, [], capabilities); terminal.loadAddon(quickFixAddon); @@ -96,23 +94,23 @@ suite('QuickFixAddon', () => { }); suite('returns undefined when', () => { test('output does not match', async () => { - strictEqual(await (getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitSimilarOutputRegex, exitCode, [`invalid output`]), expectedMap, openerService, labelService)), undefined); + strictEqual(await (getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitSimilarOutputRegex, exitCode, [`invalid output`]), expectedMap, commandService, openerService, labelService)), undefined); }); test('command does not match', async () => { - strictEqual(await (getQuickFixesForCommand([], terminal, createCommand(`gt sttatus`, output, GitSimilarOutputRegex, exitCode, outputLines), expectedMap, openerService, labelService)), undefined); + strictEqual(await (getQuickFixesForCommand([], terminal, createCommand(`gt sttatus`, output, GitSimilarOutputRegex, exitCode, outputLines), expectedMap, commandService, openerService, labelService)), undefined); }); }); suite('returns actions when', () => { test('expected unix exit code', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitSimilarOutputRegex, exitCode, outputLines), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitSimilarOutputRegex, exitCode, outputLines), expectedMap, commandService, openerService, labelService)), actions); }); test('matching exit status', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitSimilarOutputRegex, 2, outputLines), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitSimilarOutputRegex, 2, outputLines), expectedMap, commandService, openerService, labelService)), actions); }); }); suite('returns match', () => { test('returns match', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitSimilarOutputRegex, exitCode, outputLines), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitSimilarOutputRegex, exitCode, outputLines), expectedMap, commandService, openerService, labelService)), actions); }); test('returns multiple match', async () => { @@ -133,13 +131,13 @@ suite('QuickFixAddon', () => { tooltip: 'Run: git push', command: 'git push' }]; - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand('git pu', output, GitSimilarOutputRegex, exitCode, output.split('\n')), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand('git pu', output, GitSimilarOutputRegex, exitCode, output.split('\n')), expectedMap, commandService, openerService, labelService)), actions); }); test('passes any arguments through', async () => { output = `git: 'checkoutt' is not a git command. See 'git --help'. The most similar commands are checkout`; - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand('git checkoutt .', output, GitSimilarOutputRegex, exitCode, output.split('\n')), expectedMap, openerService, labelService)), [{ + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand('git checkoutt .', output, GitSimilarOutputRegex, exitCode, output.split('\n')), expectedMap, commandService, openerService, labelService)), [{ id: 'Git Similar', enabled: true, label: 'Run: git checkout .', @@ -168,18 +166,18 @@ suite('QuickFixAddon', () => { }); suite('returns undefined when', () => { test('output does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitTwoDashesRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitTwoDashesRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); test('command does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`gt sttatus`, output, GitTwoDashesRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`gt sttatus`, output, GitTwoDashesRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); }); suite('returns actions when', () => { test('expected unix exit code', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitTwoDashesRegex, exitCode), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitTwoDashesRegex, exitCode), expectedMap, commandService, openerService, labelService)), actions); }); test('matching exit status', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitTwoDashesRegex, 2), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitTwoDashesRegex, 2), expectedMap, commandService, openerService, labelService)), actions); }); }); }); @@ -209,17 +207,17 @@ suite('QuickFixAddon', () => { enabled: true }]; setup(() => { - const command = freePort(terminalInstance); + const command = freePort(() => Promise.resolve()); expectedMap.set(command.commandLineMatcher.toString(), [command]); quickFixAddon.registerCommandFinishedListener(command); }); suite('returns undefined when', () => { test('output does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(portCommand, `invalid output`, FreePortOutputRegex), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(portCommand, `invalid output`, FreePortOutputRegex), expectedMap, commandService, openerService, labelService)), undefined); }); }); test('returns actions', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(portCommand, output, FreePortOutputRegex), expectedMap, openerService, labelService)), actionOptions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(portCommand, output, FreePortOutputRegex), expectedMap, commandService, openerService, labelService)), actionOptions); }); }); } @@ -246,18 +244,18 @@ suite('QuickFixAddon', () => { }); suite('returns undefined when', () => { test('output does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitPushOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitPushOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); test('command does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`git status`, output, GitPushOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`git status`, output, GitPushOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); }); suite('returns actions when', () => { test('expected unix exit code', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, exitCode), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), actions); }); test('matching exit status', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, 2), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, 2), expectedMap, commandService, openerService, labelService)), actions); }); }); }); @@ -287,18 +285,18 @@ suite('QuickFixAddon', () => { }); suite('returns undefined when', () => { test('output does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitCreatePrOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitCreatePrOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); test('command does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`git status`, output, GitCreatePrOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`git status`, output, GitCreatePrOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); test('failure exit status', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitCreatePrOutputRegex, 2), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitCreatePrOutputRegex, 2), expectedMap, commandService, openerService, labelService)), undefined); }); }); suite('returns actions when', () => { test('expected unix exit code', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitCreatePrOutputRegex, exitCode), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitCreatePrOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), actions); }); }); }); @@ -326,18 +324,18 @@ suite('QuickFixAddon', () => { }); suite('returns undefined when', () => { test('output does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitPushOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, GitPushOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); test('command does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`git status`, output, GitPushOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(`git status`, output, GitPushOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); }); suite('returns actions when', () => { test('expected unix exit code', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, exitCode), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), actions); }); test('matching exit status', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, 2), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, GitPushOutputRegex, 2), expectedMap, commandService, openerService, labelService)), actions); }); }); }); @@ -386,10 +384,10 @@ suite('QuickFixAddon', () => { expectedMap.set(pushCommand.commandLineMatcher.toString(), [pushCommand]); }); test('returns undefined when output does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, PwshGeneralErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, PwshGeneralErrorOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); test('returns actions when output matches', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, PwshGeneralErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, PwshGeneralErrorOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), actions); }); }); suite('Unix cmd-not-found', () => { @@ -430,10 +428,10 @@ suite('QuickFixAddon', () => { expectedMap.set(pushCommand.commandLineMatcher.toString(), [pushCommand]); }); test('returns undefined when output does not match', async () => { - strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, PwshUnixCommandNotFoundErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined); + strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, PwshUnixCommandNotFoundErrorOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), undefined); }); test('returns actions when output matches', async () => { - assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, PwshUnixCommandNotFoundErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), actions); + assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, PwshUnixCommandNotFoundErrorOutputRegex, exitCode), expectedMap, commandService, openerService, labelService)), actions); }); }); }); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 5043900e175..6223c1c211f 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -74,7 +74,8 @@ import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/vie import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IDetachedXtermTerminal, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { DetachedProcessInfo } from 'vs/workbench/contrib/terminal/browser/detachedTerminal'; +import { IDetachedTerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; @@ -121,12 +122,12 @@ class MessageSubject { }; } - constructor(public readonly resultId: string, test: TestResultItem, public readonly taskIndex: number, public readonly messageIndex: number) { + constructor(public readonly result: ITestResult, test: TestResultItem, public readonly taskIndex: number, public readonly messageIndex: number) { this.test = test.item; const messages = test.tasks[taskIndex].messages; this.messageIndex = messageIndex; - const parts = { messageIndex, resultId, taskIndex, testExtId: test.item.extId }; + const parts = { messageIndex, resultId: result.id, taskIndex, testExtId: test.item.extId }; this.expectedUri = buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }); this.actualUri = buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }); this.messageUri = buildTestUri({ ...parts, type: TestUriType.ResultMessage }); @@ -140,30 +141,27 @@ class TaskSubject { public readonly outputUri: URI; public readonly revealLocation: undefined; - constructor(public readonly resultId: string, public readonly taskIndex: number) { - this.outputUri = buildTestUri({ resultId, taskIndex, type: TestUriType.TaskOutput }); + constructor(public readonly result: ITestResult, public readonly taskIndex: number) { + this.outputUri = buildTestUri({ resultId: result.id, taskIndex, type: TestUriType.TaskOutput }); } } class TestOutputSubject { + public readonly outputUri: URI; public readonly revealLocation: undefined; - public get outputUri() { - return buildTestUri({ resultId: this.resultId, taskIndex: this.taskIndex, testExtId: this.test.item.extId, type: TestUriType.TestOutput }); - } - - constructor(public readonly resultId: string, public readonly taskIndex: number, public readonly task: ITestRunTask, public readonly test: TestResultItem) { + constructor(public readonly result: ITestResult, public readonly taskIndex: number, public readonly task: ITestRunTask, public readonly test: TestResultItem) { + this.outputUri = buildTestUri({ resultId: this.result.id, taskIndex: this.taskIndex, testExtId: this.test.item.extId, type: TestUriType.TestOutput }); } } type InspectSubject = MessageSubject | TaskSubject | TestOutputSubject; -const equalsSubject = (a: InspectSubject, b: InspectSubject) => - a.resultId === b.resultId && a.taskIndex === b.taskIndex && ( - (a instanceof MessageSubject && b instanceof MessageSubject && a.message === b.message) || - (a instanceof TaskSubject && b instanceof TaskSubject) || - (a instanceof TestOutputSubject && b instanceof TestOutputSubject && a.test === b.test) - ); +const equalsSubject = (a: InspectSubject, b: InspectSubject) => ( + (a instanceof MessageSubject && b instanceof MessageSubject && a.message === b.message) || + (a instanceof TaskSubject && b instanceof TaskSubject && a.result === b.result && a.taskIndex === b.taskIndex) || + (a instanceof TestOutputSubject && b instanceof TestOutputSubject && a.test === b.test && a.taskIndex === b.taskIndex) +); /** Iterates through every message in every result */ function* allMessages(results: readonly ITestResult[]) { @@ -626,7 +624,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo let found = false; for (const { messageIndex, taskIndex, result, test } of allMessages(this.testResults.results)) { - if (subject instanceof TaskSubject && result.id === subject.resultId) { + if (subject instanceof TaskSubject && result.id === subject.result.id) { found = true; // open the first message found in the current result } @@ -641,11 +639,11 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo return; } - if (subject instanceof TestOutputSubject && subject.test.item.extId === test.item.extId && subject.taskIndex === taskIndex && subject.resultId === result.id) { + if (subject instanceof TestOutputSubject && subject.test.item.extId === test.item.extId && subject.taskIndex === taskIndex && subject.result.id === result.id) { found = true; } - if (subject instanceof MessageSubject && subject.test.extId === test.item.extId && subject.messageIndex === messageIndex && subject.taskIndex === taskIndex && subject.resultId === result.id) { + if (subject instanceof MessageSubject && subject.test.extId === test.item.extId && subject.messageIndex === messageIndex && subject.taskIndex === taskIndex && subject.result.id === result.id) { found = true; } } @@ -663,20 +661,20 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo let previous: { messageIndex: number; taskIndex: number; result: ITestResult; test: TestResultItem } | undefined; for (const m of allMessages(this.testResults.results)) { if (subject instanceof TaskSubject) { - if (m.result.id === subject.resultId) { + if (m.result.id === subject.result.id) { break; } continue; } if (subject instanceof TestOutputSubject) { - if (m.test.item.extId === subject.test.item.extId && m.result.id === subject.resultId && m.taskIndex === subject.taskIndex) { + if (m.test.item.extId === subject.test.item.extId && m.result.id === subject.result.id && m.taskIndex === subject.taskIndex) { break; } continue; } - if (subject.test.extId === m.test.item.extId && subject.messageIndex === m.messageIndex && subject.taskIndex === m.taskIndex && subject.resultId === m.result.id) { + if (subject.test.extId === m.test.item.extId && subject.messageIndex === m.messageIndex && subject.taskIndex === m.taskIndex && subject.result.id === m.result.id) { break; } @@ -732,25 +730,29 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo return undefined; } + const result = this.testResults.results.find(r => r.id === parts.resultId); + if (!result) { + return; + } + if (parts.type === TestUriType.TaskOutput) { - return new TaskSubject(parts.resultId, parts.taskIndex); + return new TaskSubject(result, parts.taskIndex); } if (parts.type === TestUriType.TestOutput) { - const result = this.testResults.results.find(r => r.id === parts.resultId); - const test = result?.getStateById(parts.testExtId); - const task = result?.tasks[parts.taskIndex]; + const test = result.getStateById(parts.testExtId); + const task = result.tasks[parts.taskIndex]; if (!test || !task) { return; } - return new TestOutputSubject(parts.resultId, parts.taskIndex, task, test); + return new TestOutputSubject(result, parts.taskIndex, task, test); } - const { resultId, testExtId, taskIndex, messageIndex } = parts; - const test = this.testResults.getResult(parts.resultId)?.getStateById(testExtId); + const { testExtId, taskIndex, messageIndex } = parts; + const test = result?.getStateById(testExtId); if (!test || !test.tasks[parts.taskIndex]) { return; } - return new MessageSubject(resultId, test, taskIndex, messageIndex); + return new MessageSubject(result, test, taskIndex, messageIndex); } } @@ -758,7 +760,11 @@ class TestResultsViewContent extends Disposable { private static lastSplitWidth?: number; private readonly didReveal = this._register(new Emitter<{ subject: InspectSubject; preserveFocus: boolean }>()); - private readonly clickMenu = this._register(new MutableDisposable()); + private readonly currentSubjectStore = this._register(new DisposableStore()); + private messageContextKeyService!: IContextKeyService; + private contextKeyTestMessage!: IContextKey; + private contextKeyResultOutdated!: IContextKey; + private dimension?: dom.Dimension; private splitView!: SplitView; private messageContainer!: HTMLElement; @@ -797,6 +803,10 @@ class TestResultsViewContent extends Disposable { this._register(this.instantiationService.createInstance(PlainTextMessagePeek, this.editor, messageContainer)), ]; + this.messageContextKeyService = this._register(this.contextKeyService.createScoped(this.messageContainer)); + this.contextKeyTestMessage = TestingContextKeys.testMessageContext.bindTo(this.messageContextKeyService); + this.contextKeyResultOutdated = TestingContextKeys.testResultOutdated.bindTo(this.messageContextKeyService); + const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree')); const tree = this._register(this.instantiationService.createInstance( OutputPeekTree, @@ -860,21 +870,45 @@ class TestResultsViewContent extends Disposable { return this.contentProvidersUpdateLimiter.queue(async () => { await Promise.all(this.contentProviders.map(p => p.update(opts.subject))); - if (opts.subject instanceof MessageSubject) { - const contextOverlay = this.contextKeyService.createOverlay([[TestingContextKeys.testMessageContext.key, opts.subject.contextValue]]); - this.clickMenu.value = this.instantiationService - .createChild(new ServiceCollection([IContextKeyService, contextOverlay])) - .createInstance(FloatingClickMenu, { - container: this.messageContainer, - menuId: MenuId.TestMessageContent, - getActionArg: () => (opts.subject as MessageSubject).context, - }); - } else { - this.clickMenu.clear(); - } + this.currentSubjectStore.clear(); + this.populateFloatingClick(opts.subject); }); } + private populateFloatingClick(subject: InspectSubject) { + if (!(subject instanceof MessageSubject)) { + return; + } + + this.currentSubjectStore.add(toDisposable(() => { + this.contextKeyResultOutdated.reset(); + this.contextKeyTestMessage.reset(); + })); + + this.contextKeyTestMessage.set(subject.contextValue || ''); + if (subject.result instanceof LiveTestResult) { + this.contextKeyResultOutdated.set(subject.result.getStateById(subject.test.extId)?.retired ?? false); + this.currentSubjectStore.add(subject.result.onChange(ev => { + if (ev.item.item.extId === subject.test.extId) { + this.contextKeyResultOutdated.set(ev.item.retired ?? false); + } + })); + } else { + this.contextKeyResultOutdated.set(true); + } + + + this.currentSubjectStore.add( + this.instantiationService + .createChild(new ServiceCollection([IContextKeyService, this.messageContextKeyService])) + .createInstance(FloatingClickMenu, { + container: this.messageContainer, + menuId: MenuId.TestMessageContent, + getActionArg: () => (subject as MessageSubject).context, + }) + ); + } + public onLayoutBody(height: number, width: number) { this.dimension = new dom.Dimension(width, height); this.splitView.layout(width); @@ -1049,10 +1083,7 @@ export class TestResultsView extends ViewPane { return; } - this.content.reveal({ - preserveFocus, - subject: new TaskSubject(result.id, 0), - }); + this.content.reveal({ preserveFocus, subject: new TaskSubject(result, 0) }); } protected override renderBody(container: HTMLElement): void { @@ -1062,7 +1093,7 @@ export class TestResultsView extends ViewPane { const [lastResult] = this.resultService.results; if (lastResult && lastResult.tasks.length) { - this.content.reveal({ preserveFocus: true, subject: new TaskSubject(lastResult.id, 0) }); + this.content.reveal({ preserveFocus: true, subject: new TaskSubject(lastResult, 0) }); } } @@ -1322,14 +1353,13 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { private readonly xtermLayoutDelayer = this._register(new Delayer(50)); /** Active terminal instance. */ - private readonly terminal = this._register(new MutableDisposable()); + private readonly terminal = this._register(new MutableDisposable()); /** Listener for streaming result data */ private readonly outputDataListener = this._register(new MutableDisposable()); constructor( private readonly container: HTMLElement, private readonly isInPeekView: boolean, - @ITestResultService private readonly resultService: ITestResultService, @ITerminalService private readonly terminalService: ITerminalService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService, @@ -1340,11 +1370,11 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { private async makeTerminal() { const prev = this.terminal.value; if (prev) { - prev.clearBuffer(); - prev.clearSearchDecorations(); + prev.xterm.clearBuffer(); + prev.xterm.clearSearchDecorations(); // clearBuffer tries to retain the prompt line, but this doesn't exist for tests. // So clear the screen (J) and move to home (H) to ensure previous data is cleaned up. - prev.write(`\x1b[2J\x1b[0;0H`); + prev.xterm.write(`\x1b[2J\x1b[0;0H`); return prev; } @@ -1358,11 +1388,12 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { updateCwd: () => { }, }); - return this.terminal.value = await this.terminalService.createDetachedXterm({ + return this.terminal.value = await this.terminalService.createDetachedTerminal({ rows: 10, cols: 80, readonly: true, capabilities, + processInfo: new DetachedProcessInfo({ initialCwd: cwd.value }), colorProvider: { getBackgroundColor: theme => { const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); @@ -1411,17 +1442,17 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { } } }, - doListenForMoreData: (output, result, terminal) => result.onChange(e => { + doListenForMoreData: (output, result, { xterm }) => result.onChange(e => { if (e.reason === TestResultItemChangeReason.NewMessage && e.item.item.extId === testItem.extId && e.message.type === TestMessageType.Output) { for (const chunk of output.getRangeIter(e.message.offset, e.message.length)) { - terminal.write(chunk.buffer); + xterm.write(chunk.buffer); } } }), }); if (subject instanceof MessageSubject && subject.message.type === TestMessageType.Output && subject.message.marker !== undefined) { - terminal?.selectMarkedRange(getMarkId(subject.message.marker, true), getMarkId(subject.message.marker, false), /* scrollIntoView= */ true); + terminal?.xterm.selectMarkedRange(getMarkId(subject.message.marker, true), getMarkId(subject.message.marker, false), /* scrollIntoView= */ true); } } @@ -1435,7 +1466,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { this.updateCwd(Iterable.find(result.tests, t => !!t.item.uri)?.item.uri); return task.output.buffers; }, - doListenForMoreData: (task, _result, terminal) => task.output.onDidWriteData(e => terminal.write(e.buffer)), + doListenForMoreData: (task, _result, { xterm }) => task.output.onDidWriteData(e => xterm.write(e.buffer)), }); } @@ -1443,13 +1474,9 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { subject: InspectSubject; getTarget: (result: ITestResult) => T | undefined; doInitialWrite: (target: T, result: LiveTestResult) => Iterable; - doListenForMoreData: (target: T, result: LiveTestResult, terminal: IDetachedXtermTerminal) => IDisposable | undefined; + doListenForMoreData: (target: T, result: LiveTestResult, terminal: IDetachedTerminalInstance) => IDisposable | undefined; }) { - const result = this.resultService.getResult(opts.subject.resultId); - if (!result) { - return this.clear(); - } - + const result = opts.subject.result; const target = opts.getTarget(result); if (!target) { return this.clear(); @@ -1463,7 +1490,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { for (const chunk of opts.doInitialWrite(target, result)) { didWriteData ||= chunk.byteLength > 0; pendingWrites.value++; - terminal.write(chunk.buffer, () => pendingWrites.value--); + terminal.xterm.write(chunk.buffer, () => pendingWrites.value--); } } else { this.writeNotice(terminal, localize('runNoOutputForPast', 'Test output is only available for new test runs.')); @@ -1500,12 +1527,12 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { } } - private writeNotice(terminal: IDetachedXtermTerminal, str: string) { - terminal.write(`\x1b[2m${str}\x1b[0m`); + private writeNotice(terminal: IDetachedTerminalInstance, str: string) { + terminal.xterm.write(`\x1b[2m${str}\x1b[0m`); } - private attachTerminalToDom(terminal: IDetachedXtermTerminal) { - terminal.write('\x1b[?25l'); // hide cursor + private attachTerminalToDom(terminal: IDetachedTerminalInstance) { + terminal.xterm.write('\x1b[?25l'); // hide cursor requestAnimationFrame(() => this.layoutTerminal(terminal)); terminal.attachToElement(this.container, { enableGpu: false }); } @@ -1524,7 +1551,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { } private layoutTerminal( - xterm: IDetachedXtermTerminal, + { xterm }: IDetachedTerminalInstance, width = this.dimensions?.width ?? this.container.clientWidth, height = this.dimensions?.height ?? this.container.clientHeight ) { @@ -1639,13 +1666,19 @@ class TestResultElement implements ITreeElement { } class TestCaseElement implements ITreeElement { - public readonly changeEmitter = new Emitter(); - public readonly onDidChange = this.changeEmitter.event; public readonly type = 'test'; public readonly context = this.test.item.extId; public readonly id = `${this.results.id}/${this.test.item.extId}`; public readonly description?: string; + public get onDidChange() { + if (!(this.results instanceof LiveTestResult)) { + return Event.None; + } + + return Event.filter(this.results.onChange, e => e.item.item.extId === this.test.item.extId); + } + public get state() { return this.test.tasks[this.taskIndex].state; } @@ -1663,7 +1696,7 @@ class TestCaseElement implements ITreeElement { } public get outputSubject() { - return new TestOutputSubject(this.results.id, this.taskIndex, this.task, this.test); + return new TestOutputSubject(this.results, this.taskIndex, this.task, this.test); } constructor( @@ -1710,10 +1743,18 @@ class TestMessageElement implements ITreeElement { public readonly uri: URI; public readonly location?: IRichLocation; public readonly description?: string; - public readonly onDidChange = Event.None; public readonly contextValue?: string; public readonly message: ITestMessage; + public get onDidChange() { + if (!(this.result instanceof LiveTestResult)) { + return Event.None; + } + + // rerender when the test case changes so it gets retired events + return Event.filter(this.result.onChange, e => e.item.item.extId === this.test.item.extId); + } + public get context(): ITestMessageMenuArgs { return { $mid: MarshalledId.TestMessageMenuArgs, @@ -1874,7 +1915,7 @@ class OutputPeekTree extends Disposable { const disposable = new DisposableStore(); disposable.add(result.onNewTask(() => { if (result.tasks.length === 1) { - this.requestReveal.fire(new TaskSubject(result.id, 0)); // reveal the first task in new runs + this.requestReveal.fire(new TaskSubject(result, 0)); // reveal the first task in new runs } if (this.tree.hasElement(resultNode)) { @@ -1898,7 +1939,6 @@ class OutputPeekTree extends Disposable { if (e.reason === TestResultItemChangeReason.NewMessage && e.message.type === TestMessageType.Error) { this.tree.setChildren(itemNode, getTestChildren(result, e.item, index), { diffIdentityProvider }); } - itemNode.changeEmitter.fire(); return; } @@ -1954,10 +1994,10 @@ class OutputPeekTree extends Disposable { if (subject instanceof TaskSubject) { const resultItem = this.tree.getNode(null).children.find(c => { if (c.element instanceof TaskElement) { - return c.element.results.id === subject.resultId && c.element.index === subject.taskIndex; + return c.element.results.id === subject.result.id && c.element.index === subject.taskIndex; } if (c.element instanceof TestResultElement) { - return c.element.id === subject.resultId; + return c.element.id === subject.result.id; } return false; }); @@ -1993,7 +2033,7 @@ class OutputPeekTree extends Disposable { this._register(this.tree.onDidOpen(async e => { if (e.element instanceof TestMessageElement) { - this.requestReveal.fire(new MessageSubject(e.element.result.id, e.element.test, e.element.taskIndex, e.element.messageIndex)); + this.requestReveal.fire(new MessageSubject(e.element.result, e.element.test, e.element.taskIndex, e.element.messageIndex)); } })); @@ -2164,7 +2204,7 @@ class TreeActionsProvider { localize('testing.showResultOutput', "Show Result Output"), ThemeIcon.asClassName(Codicon.terminal), undefined, - () => this.requestReveal.fire(new TaskSubject(element.results.id, element.index)), + () => this.requestReveal.fire(new TaskSubject(element.results, element.index)), )); } @@ -2176,7 +2216,7 @@ class TreeActionsProvider { localize('testing.showResultOutput', "Show Result Output"), ThemeIcon.asClassName(Codicon.terminal), undefined, - () => this.requestReveal.fire(new TaskSubject(element.value.id, 0)), + () => this.requestReveal.fire(new TaskSubject(element.value, 0)), )); } @@ -2199,10 +2239,15 @@ class TreeActionsProvider { } } + if (element instanceof TestCaseElement || element instanceof TestMessageElement) { + contextKeys.push( + [TestingContextKeys.testResultOutdated.key, element.test.retired], + ...getTestItemContextOverlay(element.test, capabilities), + ); + } + if (element instanceof TestCaseElement) { const extId = element.test.item.extId; - contextKeys.push(...getTestItemContextOverlay(element.test, capabilities)); - primary.push(new Action( 'testing.outputPeek.goToFile', localize('testing.goToFile', "Go to Source"), diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index fd8c90d0ce6..eccdf3fc5a0 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -59,7 +59,11 @@ export namespace TestingContextKeys { description: localize('testing.testItemIsHidden', 'Boolean indicating whether the test item is hidden') }); export const testMessageContext = new RawContextKey('testMessage', undefined, { - type: 'boolean', + type: 'string', description: localize('testing.testMessage', 'Value set in `testMessage.contextValue`, available in editor/content and testing/message/context') }); + export const testResultOutdated = new RawContextKey('testResultOutdated', undefined, { + type: 'boolean', + description: localize('testing.testResultOutdated', 'Value available in editor/content and testing/message/context when the result is outdated') + }); } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 85ba62c640e..f0c08e1b826 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -16,7 +16,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { FileAccess } from 'vs/base/common/network'; -import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EXTENSION_INSTALL_DEP_PACK_CONTEXT, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ThemeIcon } from 'vs/base/common/themables'; import { walkthroughs } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; @@ -229,9 +229,10 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => { const hadLastFoucs = await this.hostService.hadLastFocus(); for (const e of result) { + const skipWalkthrough = e?.context?.[EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT] || e?.context?.[EXTENSION_INSTALL_DEP_PACK_CONTEXT]; // If the window had last focus and the install didn't specify to skip the walkthrough // Then add it to the sessionInstallExtensions to be opened - if (hadLastFoucs && !e?.context?.[EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]) { + if (hadLastFoucs && !skipWalkthrough) { this.sessionInstalledExtensions.add(e.identifier.id.toLowerCase()); } this.progressByEvent(`extensionInstalled:${e.identifier.id.toLowerCase()}`); diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 6d705492fe9..8bc95f4f591 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -32,7 +32,6 @@ import { isEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleF import { dirname, resolve } from 'vs/base/common/path'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND } from 'vs/workbench/common/theme'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IBannerItem, IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; @@ -527,9 +526,6 @@ export class WorkspaceTrustUXHandler extends Disposable implements IWorkbenchCon private getStatusbarEntry(trusted: boolean): IStatusbarEntry { const text = workspaceTrustToString(trusted); - const backgroundColor = { id: STATUS_BAR_PROMINENT_ITEM_BACKGROUND }; - const color = { id: STATUS_BAR_PROMINENT_ITEM_FOREGROUND }; - let ariaLabel = ''; let toolTip: IMarkdownString | string | undefined; switch (this.workspaceContextService.getWorkbenchState()) { @@ -586,8 +582,7 @@ export class WorkspaceTrustUXHandler extends Disposable implements IWorkbenchCon ariaLabel: ariaLabel, tooltip: toolTip, command: MANAGE_TRUST_COMMAND_ID, - backgroundColor, - color + kind: 'prominent' }; } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 5b625c9c856..1efcf6ba71f 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -723,118 +723,35 @@ export class NativeWindow extends Disposable { } } - // Windows 7/8/8.1 warning - if (isWindows) { - const version = this.environmentService.os.release.split('.'); - const majorVersion = version[0]; - const minorVersion = version[1]; - const eolReleases = new Map>([ - ['6', new Map([ - ['1', 'Windows 7 / Windows Server 2008 R2'], - ['2', 'Windows 8 / Windows Server 2012'], - ['3', 'Windows 8.1 / Windows Server 2012 R2'], - ])], - ]); + // Windows 32-bit warning + if (isWindows && this.environmentService.os.arch === 'ia32') { + const message = localize('windows32eolmessage', "{0} on Windows 32-bit will soon stop receiving updates. Consider upgrading to the 64-bit build.", this.productService.nameLong); + const actions = [{ + label: localize('windowseolBannerLearnMore', "Learn More"), + href: 'https://aka.ms/vscode-faq-old-windows' + }]; - // Refs https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa - if (eolReleases.get(majorVersion)?.has(minorVersion)) { - const message = localize('windowseolmessage', "{0} on {1} will soon stop receiving updates. Consider upgrading your windows version.", this.productService.nameLong, eolReleases.get(majorVersion)?.get(minorVersion)); - const actions = [{ - label: localize('windowseolBannerLearnMore', "Learn More"), - href: 'https://aka.ms/vscode-faq-old-windows' - }]; + this.bannerService.show({ + id: 'windows32eol.banner', + message, + ariaLabel: localize('windowseolarialabel', "{0}. Use navigation keys to access banner actions.", message), + actions, + icon: Codicon.warning + }); - this.bannerService.show({ - id: 'windowseol.banner', - message, - ariaLabel: localize('windowseolarialabel', "{0}. Use navigation keys to access banner actions.", message), - actions, - icon: Codicon.warning - }); - - this.notificationService.prompt( - Severity.Warning, - message, - [{ - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-old-windows')) - }], - { - neverShowAgain: { id: 'windowseol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION }, - priority: NotificationPriority.URGENT, - sticky: true - } - ); - } - - else if (this.environmentService.os.arch === 'ia32') { - const message = localize('windows32eolmessage', "{0} on Windows 32-bit will soon stop receiving updates. Consider upgrading to the 64-bit build.", this.productService.nameLong); - const actions = [{ - label: localize('windowseolBannerLearnMore', "Learn More"), - href: 'https://aka.ms/vscode-faq-old-windows' - }]; - - this.bannerService.show({ - id: 'windows32eol.banner', - message, - ariaLabel: localize('windowseolarialabel', "{0}. Use navigation keys to access banner actions.", message), - actions, - icon: Codicon.warning - }); - - this.notificationService.prompt( - Severity.Warning, - message, - [{ - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-old-windows')) - }], - { - neverShowAgain: { id: 'windows32eol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION }, - priority: NotificationPriority.URGENT, - sticky: true - } - ); - } - } - - // MacOS 10.11 and 10.12 warning - if (isMacintosh) { - const majorVersion = this.environmentService.os.release.split('.')[0]; - const eolReleases = new Map([ - ['15', 'OS X El Capitan'], - ['16', 'macOS Sierra'], - ]); - // Refs https://en.wikipedia.org/wiki/Darwin_%28operating_system%29#Release_history - if (eolReleases.has(majorVersion)) { - const message = localize('macoseolmessage', "{0} on {1} will soon stop receiving updates. Consider upgrading your macOS version.", this.productService.nameLong, eolReleases.get(majorVersion)); - const actions = [{ - label: localize('macoseolBannerLearnMore', "Learn More"), - href: 'https://aka.ms/vscode-faq-old-macOS' - }]; - - this.bannerService.show({ - id: 'macoseol.banner', - message, - ariaLabel: localize('macoseolarialabel', "{0}. Use navigation keys to access banner actions.", message), - actions, - icon: Codicon.warning - }); - - this.notificationService.prompt( - Severity.Warning, - message, - [{ - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-old-macOS')) - }], - { - neverShowAgain: { id: 'macoseol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION }, - priority: NotificationPriority.URGENT, - sticky: true - } - ); - } + this.notificationService.prompt( + Severity.Warning, + message, + [{ + label: localize('learnMore', "Learn More"), + run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-old-windows')) + }], + { + neverShowAgain: { id: 'windows32eol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION }, + priority: NotificationPriority.URGENT, + sticky: true + } + ); } // Slow shell environment progress indicator diff --git a/src/vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService.ts b/src/vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService.ts new file mode 100644 index 00000000000..bb143a48893 --- /dev/null +++ b/src/vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancelablePromise, createCancelablePromise, raceCancellablePromises, timeout } from 'vs/base/common/async'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { ILogService } from 'vs/platform/log/common/log'; + +export const IAiEmbeddingVectorService = createDecorator('IAiEmbeddingVectorService'); + +export interface IAiEmbeddingVectorService { + readonly _serviceBrand: undefined; + + isEnabled(): boolean; + getEmbeddingVector(str: string, token: CancellationToken): Promise; + getEmbeddingVector(strings: string[], token: CancellationToken): Promise; + registerAiEmbeddingVectorProvider(model: string, provider: IAiEmbeddingVectorProvider): IDisposable; +} + +export interface IAiEmbeddingVectorProvider { + provideAiEmbeddingVector(strings: string[], token: CancellationToken): Promise; +} + +export class AiEmbeddingVectorService implements IAiEmbeddingVectorService { + readonly _serviceBrand: undefined; + + static readonly DEFAULT_TIMEOUT = 1000 * 10; // 10 seconds + + private readonly _providers: IAiEmbeddingVectorProvider[] = []; + + constructor(@ILogService private readonly logService: ILogService) { } + + isEnabled(): boolean { + return this._providers.length > 0; + } + + registerAiEmbeddingVectorProvider(model: string, provider: IAiEmbeddingVectorProvider): IDisposable { + this._providers.push(provider); + return { + dispose: () => { + const index = this._providers.indexOf(provider); + if (index >= 0) { + this._providers.splice(index, 1); + } + } + }; + } + + getEmbeddingVector(str: string, token: CancellationToken): Promise; + getEmbeddingVector(strings: string[], token: CancellationToken): Promise; + async getEmbeddingVector(strings: string | string[], token: CancellationToken): Promise { + if (this._providers.length === 0) { + throw new Error('No embedding vector providers registered'); + } + + const stopwatch = StopWatch.create(); + + const cancellablePromises: Array> = []; + + const timer = timeout(AiEmbeddingVectorService.DEFAULT_TIMEOUT); + const disposable = token.onCancellationRequested(() => { + disposable.dispose(); + timer.cancel(); + }); + + for (const provider of this._providers) { + cancellablePromises.push(createCancelablePromise(async t => { + try { + return await provider.provideAiEmbeddingVector( + Array.isArray(strings) ? strings : [strings], + t + ); + } catch (e) { + // logged in extension host + } + // Wait for the timer to finish to allow for another provider to resolve. + // Alternatively, if something resolved, or we've timed out, this will throw + // as expected. + await timer; + throw new Error('Embedding vector provider timed out'); + })); + } + + cancellablePromises.push(createCancelablePromise(async (t) => { + const disposable = t.onCancellationRequested(() => { + timer.cancel(); + disposable.dispose(); + }); + await timer; + throw new Error('Embedding vector provider timed out'); + })); + + try { + const result = await raceCancellablePromises(cancellablePromises); + + // If we have a single result, return it directly, otherwise return an array. + // This aligns with the API overloads. + if (result.length === 1) { + return result[0]; + } + return result; + } finally { + stopwatch.stop(); + this.logService.trace(`[AiEmbeddingVectorService]: getEmbeddingVector took ${stopwatch.elapsed()}ms`); + } + } +} + +registerSingleton(IAiEmbeddingVectorService, AiEmbeddingVectorService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation.ts b/src/vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation.ts new file mode 100644 index 00000000000..a8bddf2c1e4 --- /dev/null +++ b/src/vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IAiRelatedInformationService = createDecorator('IAiRelatedInformationService'); + +export enum RelatedInformationType { + SymbolInformation = 1, + CommandInformation = 2, + SearchInformation = 3, + SettingInformation = 4 +} + +export interface RelatedInformationResult { + type: RelatedInformationType; + weight: number; +} + +export interface CommandInformationResult extends RelatedInformationResult { + type: RelatedInformationType.CommandInformation; + command: string; +} + +export interface SettingInformationResult extends RelatedInformationResult { + type: RelatedInformationType.SettingInformation; + setting: string; +} + +export interface IAiRelatedInformationService { + readonly _serviceBrand: undefined; + + isEnabled(): boolean; + getRelatedInformation(query: string, types: RelatedInformationType[], token: CancellationToken): Promise; + registerAiRelatedInformationProvider(types: RelatedInformationType[], provider: IAiRelatedInformationProvider): IDisposable; +} + +export interface IAiRelatedInformationProvider { + provideAiRelatedInformation(query: string, types: RelatedInformationType[], token: CancellationToken): Promise; +} diff --git a/src/vs/workbench/services/aiRelatedInformation/common/aiRelatedInformationService.ts b/src/vs/workbench/services/aiRelatedInformation/common/aiRelatedInformationService.ts new file mode 100644 index 00000000000..e0309ff37d1 --- /dev/null +++ b/src/vs/workbench/services/aiRelatedInformation/common/aiRelatedInformationService.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancelablePromise, createCancelablePromise, raceCancellablePromises, timeout } from 'vs/base/common/async'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IAiRelatedInformationService, IAiRelatedInformationProvider, RelatedInformationType, RelatedInformationResult } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; + +export class AiRelatedInformationService implements IAiRelatedInformationService { + readonly _serviceBrand: undefined; + + static readonly DEFAULT_TIMEOUT = 1000 * 10; // 10 seconds + + private readonly _providers: Map = new Map(); + + constructor(@ILogService private readonly logService: ILogService) { } + + isEnabled(): boolean { + return this._providers.size > 0; + } + + registerAiRelatedInformationProvider(types: RelatedInformationType[], provider: IAiRelatedInformationProvider): IDisposable { + for (const type of types) { + const providers = this._providers.get(type) ?? []; + providers.push(provider); + this._providers.set(type, providers); + } + + return { + dispose: () => { + for (const type of types) { + const providers = this._providers.get(type) ?? []; + const index = providers.indexOf(provider); + if (index !== -1) { + providers.splice(index, 1); + } + if (providers.length === 0) { + this._providers.delete(type); + } + } + } + }; + } + + async getRelatedInformation(query: string, types: RelatedInformationType[], token: CancellationToken): Promise { + if (this._providers.size === 0) { + throw new Error('No related information providers registered'); + } + + // get providers for each type + const providers: IAiRelatedInformationProvider[] = []; + for (const type of types) { + const typeProviders = this._providers.get(type); + if (typeProviders) { + providers.push(...typeProviders); + } + } + + if (providers.length === 0) { + throw new Error('No related information providers registered for the given types'); + } + + const stopwatch = StopWatch.create(); + + const cancellablePromises: Array> = []; + + const timer = timeout(AiRelatedInformationService.DEFAULT_TIMEOUT); + const disposable = token.onCancellationRequested(() => { + disposable.dispose(); + timer.cancel(); + }); + + for (const provider of providers) { + cancellablePromises.push(createCancelablePromise(async t => { + try { + const result = await provider.provideAiRelatedInformation(query, types, t); + // double filter just in case + return result.filter(r => types.includes(r.type)); + } catch (e) { + // logged in extension host + } + // Wait for the timer to finish to allow for another provider to resolve. + // Alternatively, if something resolved, or we've timed out, this will throw + // as expected. + await timer; + throw new Error('Related information provider timed out'); + })); + } + + cancellablePromises.push(createCancelablePromise(async (t) => { + const disposable = t.onCancellationRequested(() => { + timer.cancel(); + disposable.dispose(); + }); + await timer; + throw new Error('Related information provider timed out'); + })); + + try { + const result = await raceCancellablePromises(cancellablePromises); + return result; + } finally { + stopwatch.stop(); + this.logService.trace(`[AiRelatedInformationService]: getRelatedInformation took ${stopwatch.elapsed()}ms`); + } + } +} + +registerSingleton(IAiRelatedInformationService, AiRelatedInformationService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 8f942efdd0e..591bddbe576 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -259,8 +259,8 @@ class FileServiceBasedConfiguration extends Disposable { const resolveContents = async (resources: URI[]): Promise<(string | undefined)[]> => { return Promise.all(resources.map(async resource => { try { - const content = (await this.fileService.readFile(resource)).value.toString(); - return content; + const content = await this.fileService.readFile(resource, { atomic: true }); + return content.value.toString(); } catch (error) { this.logService.trace(`Error while resolving configuration file '${resource.toString()}': ${errors.getErrorMessage(error)}`); if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND @@ -494,7 +494,7 @@ class FileServiceBasedRemoteUserConfiguration extends Disposable { } async resolveContent(): Promise { - const content = await this.fileService.readFile(this.configurationResource); + const content = await this.fileService.readFile(this.configurationResource, { atomic: true }); return content.value.toString(); } @@ -764,7 +764,7 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable { } async resolveContent(workspaceIdentifier: IWorkspaceIdentifier): Promise { - const content = await this.fileService.readFile(workspaceIdentifier.configPath); + const content = await this.fileService.readFile(workspaceIdentifier.configPath, { atomic: true }); return content.value.toString(); } diff --git a/src/vs/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts index 2ee077e32a2..80a551f4cc3 100644 --- a/src/vs/workbench/services/credentials/browser/credentialsService.ts +++ b/src/vs/workbench/services/credentials/browser/credentialsService.ts @@ -30,15 +30,18 @@ export class BrowserCredentialsService extends Disposable implements ICredential ) { super(); - if (environmentService.remoteAuthority && !environmentService.options?.credentialsProvider) { + if ( + environmentService.remoteAuthority + && !environmentService.options?.credentialsProvider + && !environmentService.options?.secretStorageProvider + ) { // If we have a remote authority but the embedder didn't provide a credentialsProvider, // we can use the CredentialsService on the remote side const remoteCredentialsService = ProxyChannel.toService(remoteAgentService.getConnection()!.getChannel('credentials')); this.credentialsProvider = remoteCredentialsService; this._secretStoragePrefix = remoteCredentialsService.getSecretStoragePrefix(); } else { - // fall back to InMemoryCredentialsProvider if none was given to us. This should really only be used - // when running tests. + // fall back to InMemoryCredentialsProvider if none was given to us. this.credentialsProvider = environmentService.options?.credentialsProvider ?? new InMemoryCredentialsProvider(); this._secretStoragePrefix = Promise.resolve(this.productService.urlProtocol); } diff --git a/src/vs/workbench/services/editor/browser/codeEditorService.ts b/src/vs/workbench/services/editor/browser/codeEditorService.ts index 657f203312c..9930f85f58c 100644 --- a/src/vs/workbench/services/editor/browser/codeEditorService.ts +++ b/src/vs/workbench/services/editor/browser/codeEditorService.ts @@ -25,8 +25,8 @@ export class CodeEditorService extends AbstractCodeEditorService { ) { super(themeService); - this.registerCodeEditorOpenHandler(this.doOpenCodeEditor.bind(this)); - this.registerCodeEditorOpenHandler(this.doOpenCodeEditorFromDiff.bind(this)); + this._register(this.registerCodeEditorOpenHandler(this.doOpenCodeEditor.bind(this))); + this._register(this.registerCodeEditorOpenHandler(this.doOpenCodeEditorFromDiff.bind(this))); } getActiveCodeEditor(): ICodeEditor | null { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index e241e493f65..f997b3ecfbd 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -83,9 +83,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Editor & group changes this.editorGroupService.whenReady.then(() => this.onEditorGroupsReady()); - this.editorGroupService.onDidChangeActiveGroup(group => this.handleActiveEditorChange(group)); - this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView)); - this.editorsObserver.onDidMostRecentlyActiveEditorsChange(() => this._onDidMostRecentlyActiveEditorsChange.fire()); + this._register(this.editorGroupService.onDidChangeActiveGroup(group => this.handleActiveEditorChange(group))); + this._register(this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView))); + this._register(this.editorsObserver.onDidMostRecentlyActiveEditorsChange(() => this._onDidMostRecentlyActiveEditorsChange.fire())); // Out of workspace file watchers this._register(this.onDidVisibleEditorsChange(() => this.handleVisibleEditorsChange())); diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index f100b353488..fcff4bbe95c 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -251,7 +251,7 @@ class InstallExtensionTask extends AbstractExtensionTask implem constructor( manifest: IExtensionManifest, private readonly extension: URI | IGalleryExtension, - private readonly options: InstallExtensionTaskOptions, + readonly options: InstallExtensionTaskOptions, private readonly webExtensionsScannerService: IWebExtensionsScannerService, private readonly userDataProfilesService: IUserDataProfilesService, ) { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 8f6f09ae49d..03d80474dad 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -6,6 +6,7 @@ // THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. export const allApiProposals = Object.freeze({ + aiRelatedInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', @@ -13,6 +14,7 @@ export const allApiProposals = Object.freeze({ chatProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', chatRequestAccess: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatRequestAccess.d.ts', chatSlashCommands: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSlashCommands.d.ts', + chatVariables: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariables.d.ts', codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', commentsDraftState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', @@ -57,6 +59,7 @@ export const allApiProposals = Object.freeze({ interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', languageConfigurationAutoClosingPairs: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts', + mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', notebookCodeActions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCodeActions.d.ts', notebookControllerAffinityHidden: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index eb3cd0cb738..c50429a9294 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -138,8 +138,9 @@ export interface IFilterResult { */ export enum SettingMatchType { None = 0, - WholeWordMatch = 1 << 0, - KeyMatch = 1 << 1 + RemoteMatch = 1 << 0, + WholeWordMatch = 1 << 1, + KeyMatch = 1 << 2 } export interface ISettingMatch { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 0ccee385532..fa940fa4782 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -95,6 +95,10 @@ abstract class AbstractSettingsModel extends EditorModel { // Sort by match type if the match types are not the same. // The priority of the match type is given by the SettingMatchType enum. return b.matchType - a.matchType; + } else if (a.matchType === SettingMatchType.RemoteMatch) { + // The match types are the same and are RemoteMatch. + // Sort by score. + return b.score - a.score; } else { // The match types are the same. if (a.setting.extensionInfo && b.setting.extensionInfo diff --git a/src/vs/workbench/services/quickinput/browser/quickInputService.ts b/src/vs/workbench/services/quickinput/browser/quickInputService.ts index e47d590694e..14d68592f60 100644 --- a/src/vs/workbench/services/quickinput/browser/quickInputService.ts +++ b/src/vs/workbench/services/quickinput/browser/quickInputService.ts @@ -9,7 +9,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { QuickInputController } from 'vs/platform/quickinput/browser/quickInput'; +import { QuickInputController } from 'vs/platform/quickinput/browser/quickInputController'; import { QuickInputService as BaseQuickInputService } from 'vs/platform/quickinput/browser/quickInputService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; diff --git a/src/vs/workbench/services/remote/common/tunnelModel.ts b/src/vs/workbench/services/remote/common/tunnelModel.ts index 5a23fcaf61f..31ce3fca59f 100644 --- a/src/vs/workbench/services/remote/common/tunnelModel.ts +++ b/src/vs/workbench/services/remote/common/tunnelModel.ts @@ -50,7 +50,7 @@ export interface Tunnel { } export function parseAddress(address: string): { host: string; port: number } | undefined { - const matches = address.match(/^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*:)?([0-9]+)$/); + const matches = address.match(/^([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*:)?([0-9]+)$/); if (!matches) { return undefined; } @@ -404,6 +404,8 @@ export class TunnelModel extends Disposable { public readonly configPortsAttributes: PortsAttributes; private restoreListener: DisposableStore | undefined = undefined; private knownPortsRestoreValue: string | undefined; + private restoreComplete = false; + private onRestoreComplete: Emitter = new Emitter(); private unrestoredExtensionTunnels: Map = new Map(); private sessionCachedProperties: Map> = new Map(); @@ -558,7 +560,7 @@ export class TunnelModel extends Disposable { const alreadyForwarded = mapHasAddressLocalhostOrAllInterfaces(this.detected, tunnel.remoteHost, tunnel.remotePort); // Extension forwarded ports should only be updated, not restored. if ((tunnel.source.source !== TunnelSource.Extension && !alreadyForwarded) || (tunnel.source.source === TunnelSource.Extension && alreadyForwarded)) { - await this.forward({ + await this.doForward({ remote: { host: tunnel.remoteHost, port: tunnel.remotePort }, local: tunnel.localPort, name: tunnel.name, @@ -573,6 +575,9 @@ export class TunnelModel extends Disposable { } } + this.restoreComplete = true; + this.onRestoreComplete.fire(); + if (!this.restoreListener) { // It's possible that at restore time the value hasn't synced. const key = await this.getStorageKey(); @@ -620,6 +625,13 @@ export class TunnelModel extends Disposable { } async forward(tunnelProperties: TunnelProperties, attributes?: Attributes | null): Promise { + if (!this.restoreComplete && this.environmentService.remoteAuthority) { + await Event.toPromise(this.onRestoreComplete.event); + } + return this.doForward(tunnelProperties, attributes); + } + + private async doForward(tunnelProperties: TunnelProperties, attributes?: Attributes | null): Promise { await this.extensionService.activateByEvent(ACTIVATION_EVENT); const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, tunnelProperties.remote.host, tunnelProperties.remote.port); @@ -721,7 +733,7 @@ export class TunnelModel extends Disposable { } case MergedAttributeAction.Reopen: { await this.close(existingTunnel.remoteHost, existingTunnel.remotePort, TunnelCloseReason.User); - await this.forward(tunnelProperties, attributes); + await this.doForward(tunnelProperties, attributes); } } @@ -874,7 +886,7 @@ export class TunnelModel extends Disposable { for (const forwarded of tunnels) { const attributes = allAttributes.get(forwarded.remotePort); if ((attributes?.protocol || (forwarded.protocol !== TunnelProtocol.Http)) && (attributes?.protocol !== forwarded.protocol)) { - await this.forward({ + await this.doForward({ remote: { host: forwarded.remoteHost, port: forwarded.remotePort }, local: forwarded.localPort, name: forwarded.name, diff --git a/src/vs/workbench/services/semanticSimilarity/common/semanticSimilarityService.ts b/src/vs/workbench/services/semanticSimilarity/common/semanticSimilarityService.ts index ff203430c99..e322503d117 100644 --- a/src/vs/workbench/services/semanticSimilarity/common/semanticSimilarityService.ts +++ b/src/vs/workbench/services/semanticSimilarity/common/semanticSimilarityService.ts @@ -11,8 +11,14 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { StopWatch } from 'vs/base/common/stopwatch'; import { ILogService } from 'vs/platform/log/common/log'; +/** + * @deprecated Use `IAiRelatedInformationService` instead. + */ export const ISemanticSimilarityService = createDecorator('ISemanticSimilarityService'); +/** + * @deprecated Use `IAiRelatedInformationService` instead. + */ export interface ISemanticSimilarityService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index e38c45d1cf3..2e5abde6fba 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -153,6 +153,9 @@ export interface IStatusbarStyleOverride { readonly border?: ColorIdentifier; } +export type StatusbarEntryKind = 'standard' | 'warning' | 'error' | 'prominent' | 'remote' | 'offline'; +export const StatusbarEntryKinds: StatusbarEntryKind[] = ['standard', 'warning', 'error', 'prominent', 'remote', 'offline']; + /** * A declarative way of describing a status bar entry */ @@ -188,12 +191,16 @@ export interface IStatusbarEntry { readonly tooltip?: string | IMarkdownString | HTMLElement; /** - * An optional color to use for the entry + * An optional color to use for the entry. + * + * @deprecated Use `kind` instead to support themable hover styles. */ readonly color?: string | ThemeColor; /** - * An optional background color to use for the entry + * An optional background color to use for the entry. + * + * @deprecated Use `kind` instead to support themable hover styles. */ readonly backgroundColor?: string | ThemeColor; @@ -215,6 +222,11 @@ export interface IStatusbarEntry { * specified, `syncing` will be used. */ readonly showProgress?: boolean | 'syncing' | 'loading'; + + /** + * The kind of status bar entry. This applies different colors to the entry. + */ + readonly kind?: StatusbarEntryKind; } export interface IStatusbarEntryAccessor extends IDisposable { diff --git a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index 78957c01afd..188ca299d5f 100644 --- a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -54,7 +54,7 @@ export class BrowserTextFileService extends AbstractTextFileService { private registerListeners(): void { // Lifecycle - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(), 'veto.textFiles')); + this._register(this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(), 'veto.textFiles'))); } private onBeforeShutdown(): boolean { diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 45015baa458..f7a87fb1b4b 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -89,7 +89,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex private provideDecorations(): void { // Text file model decorations - this.decorationsService.registerDecorationsProvider(new class extends Disposable implements IDecorationsProvider { + const provider = this._register(new class extends Disposable implements IDecorationsProvider { readonly label = localize('textFileModelDecorations', "Text File Model Decorations"); @@ -161,6 +161,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return undefined; } }(this.files)); + + this._register(this.decorationsService.registerDecorationsProvider(provider)); } //#endregin diff --git a/src/vs/workbench/services/textfile/common/encoding.ts b/src/vs/workbench/services/textfile/common/encoding.ts index 6675f732b0e..ad67fb4f422 100644 --- a/src/vs/workbench/services/textfile/common/encoding.ts +++ b/src/vs/workbench/services/textfile/common/encoding.ts @@ -6,6 +6,7 @@ import { Readable, ReadableStream, newWriteableStream, listenStream } from 'vs/base/common/stream'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { importAMDNodeModule } from 'vs/amdX'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; export const UTF8 = 'utf8'; export const UTF8_with_bom = 'utf8bom'; @@ -124,6 +125,8 @@ export function toDecodeStream(source: VSBufferReadableStream, options: IDecodeS let decoder: IDecoderStream | undefined = undefined; + const cts = new CancellationTokenSource(); + const createDecoder = async () => { try { @@ -158,14 +161,14 @@ export function toDecodeStream(source: VSBufferReadableStream, options: IDecodeS } catch (error) { // Stop handling anything from the source and target - sourceListener?.dispose(); + cts.cancel(); target.destroy(); reject(error); } }; - const sourceListener = listenStream(source, { + listenStream(source, { onData: async chunk => { // if the decoder is ready, we just write directly @@ -205,7 +208,7 @@ export function toDecodeStream(source: VSBufferReadableStream, options: IDecodeS // end the target with the remainders of the decoder target.end(decoder?.end()); } - }); + }, cts.token); }); } diff --git a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts index ab3cb0a0e8a..3dc16d1a46e 100644 --- a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts +++ b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts @@ -60,11 +60,15 @@ export class TextFileSaveParticipant extends Disposable { model.textEditorModel?.pushStackElement(); }, () => { // user cancel - cts.dispose(true); + cts.cancel(); + }).finally(() => { + cts.dispose(); }); } override dispose(): void { this.saveParticipants.splice(0, this.saveParticipants.length); + + super.dispose(); } } diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 7595d417a5f..7ca2a09bfb1 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -548,7 +548,7 @@ suite('Files - TextFileEditorModel', () => { test('Update Dirty', async function () { let eventCounter = 0; - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); model.setDirty(true); assert.ok(!model.isDirty()); // needs to be resolved @@ -638,8 +638,8 @@ suite('Files - TextFileEditorModel', () => { }); test('save() and isDirty() - proper with check for mtimes', async function () { - const input1 = createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async2.txt')); - const input2 = createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async.txt')); + const input1 = disposables.add(createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async2.txt'))); + const input2 = disposables.add(createFileEditorInput(instantiationService, toResource.call(this, '/path/index_async.txt'))); const model1 = await input1.resolve() as TextFileEditorModel; const model2 = await input2.resolve() as TextFileEditorModel; diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 93d906d886e..4b745f7af41 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -419,7 +419,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt startColumn: 1, endColumn: UntitledTextEditorModel.FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH + 1 // first cap at FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH }) - .trim().replace(/\s+/g, ' '); // normalize whitespaces + .trim().replace(/\s+/g, ' ') // normalize whitespaces + .replace(/\u202E/g, ''); // drop Right-to-Left Override character (#190133) firstLineText = firstLineText.substr(0, getCharContainingOffset( // finally cap at FIRST_LINE_NAME_MAX_LENGTH (grapheme aware #111235) firstLineText, UntitledTextEditorModel.FIRST_LINE_NAME_MAX_LENGTH)[0] diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index c724a93fd1b..935b78a3dfd 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -542,10 +542,14 @@ suite('Untitled text editors', () => { assert.strictEqual(input.getName(), '123456789012345678901234567890123456789'); assert.strictEqual(model.name, '123456789012345678901234567890123456789'); - assert.strictEqual(counter, 6); + model.textEditorModel?.setValue('hello\u202Eworld'); // do not allow RTL in names (#190133) + assert.strictEqual(input.getName(), 'helloworld'); + assert.strictEqual(model.name, 'helloworld'); + + assert.strictEqual(counter, 7); model.textEditorModel?.setValue('Hello\nWorld'); - assert.strictEqual(counter, 7); + assert.strictEqual(counter, 8); function createSingleEditOp(text: string, positionLineNumber: number, positionColumn: number, selectionLineNumber: number = positionLineNumber, selectionColumn: number = positionColumn): ISingleEditOperation { const range = new Range( @@ -563,7 +567,7 @@ suite('Untitled text editors', () => { } model.textEditorModel?.applyEdits([createSingleEditOp('hello', 2, 2)]); - assert.strictEqual(counter, 7); // change was not on first line + assert.strictEqual(counter, 8); // change was not on first line input.dispose(); model.dispose(); diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts index 18be856eb47..11bddb67625 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts @@ -69,5 +69,7 @@ export class StoredFileWorkingCopySaveParticipant extends Disposable { override dispose(): void { this.saveParticipants.splice(0, this.saveParticipants.length); + + super.dispose(); } } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts index e75246b8b83..f3f4dd8f05e 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts @@ -46,5 +46,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable { override dispose(): void { this.participants.clear(); + + super.dispose(); } } diff --git a/src/vs/workbench/test/browser/codeeditor.test.ts b/src/vs/workbench/test/browser/codeeditor.test.ts index 75f27d69dca..6bea2f87b2e 100644 --- a/src/vs/workbench/test/browser/codeeditor.test.ts +++ b/src/vs/workbench/test/browser/codeeditor.test.ts @@ -25,6 +25,7 @@ import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Editor - Range decorations', () => { @@ -43,13 +44,13 @@ suite('Editor - Range decorations', () => { instantiationService.stub(ILanguageService, LanguageService); instantiationService.stub(IModelService, stubModelService(instantiationService)); text = 'LINE1' + '\n' + 'LINE2' + '\n' + 'LINE3' + '\n' + 'LINE4' + '\r\n' + 'LINE5'; - model = aModel(URI.file('some_file')); - codeEditor = createTestCodeEditor(model); + model = disposables.add(aModel(URI.file('some_file'))); + codeEditor = disposables.add(createTestCodeEditor(model)); instantiationService.stub(IEditorService, 'activeEditor', { get resource() { return codeEditor.getModel()!.uri; } }); instantiationService.stub(IEditorService, 'activeTextEditorControl', codeEditor); - testObject = instantiationService.createInstance(RangeHighlightDecorations); + testObject = disposables.add(instantiationService.createInstance(RangeHighlightDecorations)); }); teardown(() => { @@ -58,6 +59,8 @@ suite('Editor - Range decorations', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + test('highlight range for the resource if it is an active editor', function () { const range: IRange = new Range(1, 1, 1, 1); testObject.highlightRange({ resource: model.uri, range }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 52a5c1aa21d..c566bc46401 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -95,7 +95,7 @@ import { CodeEditorService } from 'vs/workbench/services/editor/browser/codeEdit import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quickInputService'; import { IListService } from 'vs/platform/list/browser/listService'; import { win32, posix } from 'vs/base/common/path'; @@ -283,15 +283,16 @@ export function workbenchInstantiationService( instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); const themeService = new TestThemeService(); instantiationService.stub(IThemeService, themeService); - instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); + instantiationService.stub(ILanguageConfigurationService, disposables.add(new TestLanguageConfigurationService())); instantiationService.stub(IModelService, disposables.add(instantiationService.createInstance(ModelService))); const fileService = overrides?.fileService ? overrides.fileService(instantiationService) : new TestFileService(); instantiationService.stub(IFileService, fileService); const uriIdentityService = new UriIdentityService(fileService); + disposables.add(uriIdentityService); instantiationService.stub(IFilesConfigurationService, disposables.add(new TestFilesConfigurationService(contextKeyService, configService, workspaceContextService, environmentService, uriIdentityService, fileService))); instantiationService.stub(IUriIdentityService, uriIdentityService); - const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, uriIdentityService, new NullLogService())); - instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService)); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, new NullLogService()))); + instantiationService.stub(IUserDataProfileService, disposables.add(new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService))); instantiationService.stub(IWorkingCopyBackupService, overrides?.workingCopyBackupService ? overrides?.workingCopyBackupService(instantiationService) : new TestWorkingCopyBackupService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(INotificationService, new TestNotificationService()); @@ -305,7 +306,7 @@ export function workbenchInstantiationService( instantiationService.stub(ITextFileService, overrides?.textFileService ? overrides.textFileService(instantiationService) : disposables.add(instantiationService.createInstance(TestTextFileService))); instantiationService.stub(IHostService, instantiationService.createInstance(TestHostService)); instantiationService.stub(ITextModelService, disposables.add(instantiationService.createInstance(TextModelResolverService))); - instantiationService.stub(ILoggerService, new TestLoggerService(TestEnvironmentService.logsHome)); + instantiationService.stub(ILoggerService, disposables.add(new TestLoggerService(TestEnvironmentService.logsHome))); instantiationService.stub(ILogService, new NullLogService()); const editorGroupService = new TestEditorGroupsService([new TestEditorGroupView(0)]); instantiationService.stub(IEditorGroupsService, editorGroupService); @@ -314,10 +315,10 @@ export function workbenchInstantiationService( instantiationService.stub(IEditorService, editorService); instantiationService.stub(IWorkingCopyEditorService, disposables.add(instantiationService.createInstance(WorkingCopyEditorService))); instantiationService.stub(IEditorResolverService, disposables.add(instantiationService.createInstance(EditorResolverService))); - const textEditorService = overrides?.textEditorService ? overrides.textEditorService(instantiationService) : instantiationService.createInstance(TextEditorService); + const textEditorService = overrides?.textEditorService ? overrides.textEditorService(instantiationService) : disposables.add(instantiationService.createInstance(TextEditorService)); instantiationService.stub(ITextEditorService, textEditorService); instantiationService.stub(ICodeEditorService, disposables.add(new CodeEditorService(editorService, themeService, configService))); - instantiationService.stub(IPaneCompositePartService, new TestPaneCompositeService()); + instantiationService.stub(IPaneCompositePartService, disposables.add(new TestPaneCompositeService())); instantiationService.stub(IListService, new TestListService()); const hoverService = instantiationService.stub(IHoverService, instantiationService.createInstance(TestHoverService)); instantiationService.stub(IQuickInputService, disposables.add(new QuickInputService(configService, instantiationService, keybindingService, contextKeyService, themeService, layoutService, hoverService))); @@ -794,6 +795,8 @@ export class TestViewsService implements IViewsService { onDidChangeViewVisibilityEmitter = new Emitter<{ id: string; visible: boolean }>(); onDidChangeViewVisibility = this.onDidChangeViewVisibilityEmitter.event; + onDidChangeFocusedViewEmitter = new Emitter(); + onDidChangeFocusedView = this.onDidChangeFocusedViewEmitter.event; isViewVisible(id: string): boolean { return true; } getActiveViewWithId(id: string): T | null { return null; } getViewWithId(id: string): T | null { return null; } @@ -801,6 +804,7 @@ export class TestViewsService implements IViewsService { closeView(id: string): void { } getViewProgressIndicator(id: string) { return null!; } getActiveViewPaneContainerWithId(id: string) { return null; } + getFocusedViewName(): string { return ''; } } export class TestEditorGroupsService implements IEditorGroupsService { @@ -1933,6 +1937,7 @@ export class TestQuickInputService implements IQuickInputService { createQuickPick(): IQuickPick { throw new Error('not implemented.'); } createInputBox(): IInputBox { throw new Error('not implemented.'); } + createQuickWidget(): IQuickWidget { throw new Error('Method not implemented.'); } focus(): void { throw new Error('not implemented.'); } toggle(): void { throw new Error('not implemented.'); } navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { throw new Error('not implemented.'); } diff --git a/src/vs/workbench/test/common/utils.ts b/src/vs/workbench/test/common/utils.ts index 498c5c499fe..f4a175f315a 100644 --- a/src/vs/workbench/test/common/utils.ts +++ b/src/vs/workbench/test/common/utils.ts @@ -5,7 +5,6 @@ import * as assert from 'assert'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; -// import { LanguageService } from 'vs/editor/common/services/languageServiceImpl'; /** * This function is called before test running and also again at the end of test running diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index a52b2b4c274..1f6253a29b6 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -68,6 +68,8 @@ import 'vs/workbench/services/configuration/common/jsonEditingService'; import 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import 'vs/workbench/services/editor/browser/editorService'; import 'vs/workbench/services/editor/browser/editorResolverService'; +import 'vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService'; +import 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformationService'; import 'vs/workbench/services/history/browser/historyService'; import 'vs/workbench/services/activity/browser/activityService'; import 'vs/workbench/services/keybinding/browser/keybindingService'; @@ -226,6 +228,9 @@ import 'vs/workbench/contrib/markers/browser/markers.contribution'; // Merge Editor import 'vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution'; +// Mapped Edits +import 'vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution'; + // Commands import 'vs/workbench/contrib/commands/common/commands.contribution'; diff --git a/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts b/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts new file mode 100644 index 00000000000..d3916c50608 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/190909 + + export interface SearchResult { + // from Andrea + preview: string; + resource: Uri; + location: Range; + } + + export enum RelatedInformationType { + SymbolInformation = 1, + CommandInformation = 2, + SearchInformation = 3, + SettingInformation = 4 + } + + export interface RelatedInformationResult { + type: RelatedInformationType; + weight: number; + } + + export interface SymbolInformationResult extends RelatedInformationResult { + type: RelatedInformationType.SymbolInformation; + symbolInformation: SymbolInformation; + } + + export interface CommandInformationResult extends RelatedInformationResult { + type: RelatedInformationType.CommandInformation; + command: string; + } + + export interface SettingInformationResult extends RelatedInformationResult { + type: RelatedInformationType.SettingInformation; + setting: string; + } + + export interface SearchInformationResult extends RelatedInformationResult { + type: RelatedInformationType.SearchInformation; + searchResult: SearchResult; + } + + export interface RelatedInformationProvider { + provideRelatedInformation(query: string, types: RelatedInformationType[], token: CancellationToken): ProviderResult; + } + + export interface EmbeddingVectorProvider { + provideEmbeddingVector(strings: string[], token: CancellationToken): ProviderResult; + } + + export namespace ai { + export function getRelatedInformation(query: string, types: RelatedInformationType[], token: CancellationToken): Thenable; + export function registerRelatedInformationProvider(types: RelatedInformationType[], provider: RelatedInformationProvider): Disposable; + export function registerEmbeddingVectorProvider(model: string, provider: EmbeddingVectorProvider): Disposable; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatVariables.d.ts b/src/vscode-dts/vscode.proposed.chatVariables.d.ts new file mode 100644 index 00000000000..6d9de44be86 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatVariables.d.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface InteractiveRequest { + variables: Record; + } + + export enum ChatVariableLevel { + Short = 1, + Medium = 2, + Full = 3 + } + + export interface ChatVariableValue { + level: ChatVariableLevel; + value: string; + description?: string; + } + + export interface ChatVariableContext { + message: string; + } + + export interface ChatVariableResolver { + resolve(name: string, context: ChatVariableContext, token: CancellationToken): ProviderResult; + } + + export namespace chat { + export function registerVariable(name: string, description: string, resolver: ChatVariableResolver): Disposable; + } +} diff --git a/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts b/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts index d25a92725a4..1de5bed146e 100644 --- a/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts +++ b/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts @@ -13,16 +13,12 @@ declare module 'vscode' { export interface EnvironmentVariableMutatorOptions { /** * Apply to the environment just before the process is created. - * - * Defaults to true. */ applyAtProcessCreation?: boolean; /** * Apply to the environment in the shell integration script. Note that this _will not_ apply * the mutator if shell integration is disabled or not working for some reason. - * - * Defaults to false. */ applyAtShellIntegration?: boolean; } @@ -39,17 +35,20 @@ declare module 'vscode' { export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { /** - * @param options Options applied to the mutator. + * @param options Options applied to the mutator, when not options are provided this will + * default to `{ applyAtProcessCreation: true }` */ replace(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; /** - * @param options Options applied to the mutator. + * @param options Options applied to the mutator, when not options are provided this will + * default to `{ applyAtProcessCreation: true }` */ append(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; /** - * @param options Options applied to the mutator. + * @param options Options applied to the mutator, when not options are provided this will + * default to `{ applyAtProcessCreation: true }` */ prepend(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; } diff --git a/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts b/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts index 73bd11df4e2..fb4b78e172b 100644 --- a/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts +++ b/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts @@ -7,27 +7,36 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/171173 - export interface ExtensionContext { + // export interface ExtensionContext { + // /** + // * Gets the extension's global environment variable collection for this workspace, enabling changes to be + // * applied to terminal environment variables. + // */ + // readonly environmentVariableCollection: GlobalEnvironmentVariableCollection; + // } + + export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection { /** - * Gets the extension's environment variable collection for this workspace, enabling changes - * to be applied to terminal environment variables. + * Gets scope-specific environment variable collection for the extension. This enables alterations to + * terminal environment variables solely within the designated scope, and is applied in addition to (and + * after) the global collection. * - * @deprecated Use {@link getEnvironmentVariableCollection} instead. - */ - readonly environmentVariableCollection: EnvironmentVariableCollection; - /** - * Gets the extension's environment variable collection for this scope, enabling changes - * to be applied to terminal environment variables. + * Each object obtained through this method is isolated and does not impact objects for other scopes, + * including the global collection. * * @param scope The scope to which the environment variable collection applies to. + * + * If a scope parameter is omitted, collection applicable to all relevant scopes for that parameter is + * returned. For instance, if the 'workspaceFolder' parameter is not specified, the collection that applies + * across all workspace folders will be returned. */ - getEnvironmentVariableCollection(scope?: EnvironmentVariableScope): EnvironmentVariableCollection; + getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection; } export type EnvironmentVariableScope = { /** - * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. - */ + * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. + */ workspaceFolder?: WorkspaceFolder; }; } diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index c38c4e23671..88dc3ef60c7 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -62,11 +62,6 @@ declare module 'vscode' { */ commands?: Command[]; - /** - * When set, overrides the user setting of `editor.inlineSuggest.suppressSuggestions`. - */ - suppressSuggestions?: boolean; - /** * When set and the user types a suggestion without derivating from it, the inline suggestion is not updated. * Defaults to false (might change). diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index a294597ec6a..382afeb8277 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -149,6 +149,7 @@ declare module 'vscode' { commandId: string; args?: any[]; title: string; // supports codicon strings + when?: string; } export interface InteractiveSessionSlashCommand { diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts new file mode 100644 index 00000000000..9d05e467fd9 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface RelatedContextItem { + readonly uri: Uri; + readonly range: Range; + } + + export interface MappedEditsContext { + selections: Selection[]; + + /** + * If there's no context, the array should be empty. It's also empty until we figure out how to compute this or retrieve from an extension (eg, copilot chat) + * + * TODO: it was suggested initially to be sorted from highest priority to lowest. How would it look like? + */ + related: RelatedContextItem[]; + } + + /** + * Interface for providing mapped edits for a given document. + */ + export interface MappedEditsProvider { + /** + * Provide mapped edits for a given document. + * @param document The document to provide mapped edits for. + * @param codeBlocks Code blocks that come from an LLM's reply. + * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * @param context The context for providing mapped edits. + * @param token A cancellation token. + * @returns A provider result of text edits. + */ + provideMappedEdits( + document: TextDocument, + codeBlocks: string[], + context: MappedEditsContext, + token: CancellationToken + ): ProviderResult; + } + + namespace chat { + export function registerMappedEditsProvider(documentSelector: DocumentSelector, provider: MappedEditsProvider): Disposable; + } +} diff --git a/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts b/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts index 094d1b969c4..4daa96722d8 100644 --- a/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts @@ -80,9 +80,4 @@ declare module 'vscode' { Top = 0, Bottom = 1 } - - enum TerminalQuickFixType { - Command = 0, - Opener = 1 - } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 43b6a6c6918..68401eb0eda 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -8,8 +8,9 @@ import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; -export class SettingsEditor { +const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; +export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } /** @@ -56,12 +57,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForElement('.settings-editor'); + await this.code.waitForActiveElement(SEARCH_BOX); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick('.settings-editor .suggest-input-container .monaco-editor textarea'); + + await this.code.waitAndClick(SEARCH_BOX); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 668227d2fa5..5d19c5e5205 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -14,6 +14,7 @@ const createStatsCollector = require('../../../node_modules/mocha/lib/stats-coll const MochaJUnitReporter = require('mocha-junit-reporter'); const url = require('url'); const minimatch = require('minimatch'); +const fs = require('fs'); const playwright = require('@playwright/test'); const { applyReporter } = require('../reporter'); @@ -141,9 +142,18 @@ async function runTestsInBrowser(testModules, browserType) { } const emitter = new events.EventEmitter(); - await page.exposeFunction('mocha_report', (type, data1, data2) => { - emitter.emit(type, data1, data2); - }); + + await Promise.all([ + page.exposeFunction('mocha_report', (type, data1, data2) => { + emitter.emit(type, data1, data2); + }), + // Test file operations that are common across platforms. Used for test infra, namely snapshot tests + page.exposeFunction('__readFileInTests', (path) => fs.promises.readFile(path, 'utf-8')), + page.exposeFunction('__writeFileInTests', (path, contents) => fs.promises.writeFile(path, contents)), + page.exposeFunction('__readDirInTests', (path) => fs.promises.readdir(path)), + page.exposeFunction('__unlinkInTests', (path) => fs.promises.unlink(path)), + page.exposeFunction('__mkdirPInTests', (path) => fs.promises.mkdir(path, { recursive: true })), + ]); await page.goto(target.href); diff --git a/test/unit/browser/renderer.html b/test/unit/browser/renderer.html index 540dd2a82aa..45786072900 100644 --- a/test/unit/browser/renderer.html +++ b/test/unit/browser/renderer.html @@ -116,15 +116,22 @@ runner.on('pending', test => window.mocha_report('pending', serializeRunnable(test))); }; + + async function loadModules(modules) { + for (const file of modules) { + mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha); + const m = await new Promise((resolve, reject) => require([file], resolve, err => { + console.log("BAD " + file + JSON.stringify(err, undefined, '\t')); + resolve({}); + })); + mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, mocha); + mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha); + } + } + window.loadAndRun = async function loadAndRun({ modules, grep }, manual = false) { // load - await Promise.all(modules.map(module => new Promise((resolve, reject) => { - require([module], resolve, err => { - console.log("BAD " + module + JSON.stringify(err, undefined, '\t')); - // console.log(module); - resolve({}); - }); - }))); + await loadModules(modules); // await new Promise((resolve, reject) => { // require(modules, resolve, err => { // console.log(err); diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index a91725887a5..21ef972f7c4 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -5,8 +5,9 @@ /*eslint-env mocha*/ +const fs = require('fs'); + (function () { - const fs = require('fs'); const originals = {}; let logging = false; let withStacks = false; @@ -79,6 +80,15 @@ globalThis._VSCODE_NODE_MODULES = new Proxy(Object.create(null), { get: (_target globalThis._VSCODE_PRODUCT_JSON = (require.__$__nodeRequire ?? require)('../../../product.json'); globalThis._VSCODE_PACKAGE_JSON = (require.__$__nodeRequire ?? require)('../../../package.json'); +// Test file operations that are common across platforms. Used for test infra, namely snapshot tests +Object.assign(globalThis, { + __readFileInTests: path => fs.promises.readFile(path, 'utf-8'), + __writeFileInTests: (path, contents) => fs.promises.writeFile(path, contents), + __readDirInTests: path => fs.promises.readdir(path), + __unlinkInTests: path => fs.promises.unlink(path), + __mkdirPInTests: path => fs.promises.mkdir(path, { recursive: true }), +}); + const _tests_glob = '**/test/**/*.test.js'; let loader; let _out; @@ -121,6 +131,15 @@ function loadWorkbenchTestingUtilsModule() { }); } +async function loadModules(modules) { + for (const file of modules) { + mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha); + const m = await new Promise((resolve, reject) => loader.require([file], resolve, reject)); + mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, mocha); + mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha); + } +} + function loadTestModules(opts) { if (opts.run) { @@ -130,9 +149,7 @@ function loadTestModules(opts) { file = file.replace(/\.ts$/, '.js'); return path.relative(_out, file).replace(/\.js$/, ''); }); - return new Promise((resolve, reject) => { - loader.require(modules, resolve, reject); - }); + return loadModules(modules); } const pattern = opts.runGlob || _tests_glob; @@ -146,11 +163,7 @@ function loadTestModules(opts) { const modules = files.map(file => file.replace(/\.js$/, '')); resolve(modules); }); - }).then(modules => { - return new Promise((resolve, reject) => { - loader.require(modules, resolve, reject); - }); - }); + }).then(loadModules); } function loadTests(opts) { @@ -239,6 +252,7 @@ function serializeError(err) { return { message: err.message, stack: err.stack, + snapshotPath: err.snapshotPath, actual: safeStringify({ value: err.actual }), expected: safeStringify({ value: err.expected }), uncaught: err.uncaught, diff --git a/test/unit/fullJsonStreamReporter.js b/test/unit/fullJsonStreamReporter.js index 0f3ff897c8e..07b2315a004 100644 --- a/test/unit/fullJsonStreamReporter.js +++ b/test/unit/fullJsonStreamReporter.js @@ -37,6 +37,7 @@ module.exports = class FullJsonStreamReporter extends BaseRunner { test.expected = err.expected; test.actualJSON = err.actualJSON; test.expectedJSON = err.expectedJSON; + test.snapshotPath = err.snapshotPath; test.err = err.message; test.stack = err.stack || null; writeEvent(['fail', test]); diff --git a/test/unit/node/index.js b/test/unit/node/index.js index 757df426336..b6f326a27d8 100644 --- a/test/unit/node/index.js +++ b/test/unit/node/index.js @@ -9,7 +9,7 @@ process.env.MOCHA_COLORS = '1'; // Force colors (note that this must come before any mocha imports) const assert = require('assert'); -const mocha = require('mocha'); +const Mocha = require('mocha'); const path = require('path'); const fs = require('fs'); const glob = require('glob'); @@ -64,6 +64,14 @@ function main() { globalThis._VSCODE_PRODUCT_JSON = require(`${REPO_ROOT}/product.json`); globalThis._VSCODE_PACKAGE_JSON = require(`${REPO_ROOT}/package.json`); + // Test file operations that are common across platforms. Used for test infra, namely snapshot tests + Object.assign(globalThis, { + __readFileInTests: (/** @type {string} */ path) => fs.promises.readFile(path, 'utf-8'), + __writeFileInTests: (/** @type {string} */ path, /** @type {BufferEncoding} */ contents) => fs.promises.writeFile(path, contents), + __readDirInTests: (/** @type {string} */ path) => fs.promises.readdir(path), + __unlinkInTests: (/** @type {string} */ path) => fs.promises.unlink(path), + __mkdirPInTests: (/** @type {string} */ path) => fs.promises.mkdir(path, { recursive: true }), + }); process.on('uncaughtException', function (e) { console.error(e.stack || e); @@ -127,7 +135,24 @@ function main() { return write.apply(process.stderr, args); }; - /** @type { (callback:(err:any)=>void)=>void } */ + + const runner = new Mocha({ + ui: 'tdd' + }); + + /** + * @param {string[]} modules + */ + async function loadModules(modules) { + for (const file of modules) { + runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, runner); + const m = await new Promise((resolve, reject) => loader([file], resolve, reject)); + runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, runner); + runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, runner); + } + } + + /** @type { null|((callback:(err:any)=>void)=>void) } */ let loadFunc = null; if (argv.runGlob) { @@ -140,7 +165,7 @@ function main() { return test.replace(/(\.js)|(\.d\.ts)|(\.js\.map)$/, ''); }); - loader(modulesToLoad, () => cb(null), cb); + loadModules(modulesToLoad).then(() => cb(null), cb); }; glob(argv.runGlob, { cwd: src }, function (err, files) { doRun(files); }); @@ -153,7 +178,7 @@ function main() { return path.relative(src, path.resolve(test)).replace(/(\.js)|(\.js\.map)$/, '').replace(/\\/g, '/'); }); loadFunc = (cb) => { - loader(modulesToLoad, () => cb(null), cb); + loadModules(modulesToLoad).then(() => cb(null), cb); }; } else { loadFunc = (cb) => { @@ -165,7 +190,7 @@ function main() { modules.push(file.replace(/\.js$/, '')); } } - loader(modules, function () { cb(null); }, cb); + loadModules(modules).then(() => cb(null), cb); }); }; } @@ -180,7 +205,7 @@ function main() { if (!argv.run && !argv.runGlob) { // set up last test - mocha.suite('Loader', function () { + Mocha.suite('Loader', function () { test('should not explode while loading', function () { assert.ok(!didErr, 'should not explode while loading'); }); @@ -189,7 +214,7 @@ function main() { // report failing test for every unexpected error during any of the tests const unexpectedErrors = []; - mocha.suite('Errors', function () { + Mocha.suite('Errors', function () { test('should not have unexpected errors in tests', function () { if (unexpectedErrors.length) { unexpectedErrors.forEach(function (stack) { @@ -210,7 +235,7 @@ function main() { }); // fire up mocha - mocha.run(); + runner.run(failures => process.exit(failures ? 1 : 0)); }); }); } diff --git a/yarn.lock b/yarn.lock index e06f0699b66..5595e14de37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -474,51 +474,41 @@ methods "^1.1.2" path-to-regexp "^6.2.1" -"@microsoft/1ds-core-js@4.0.1", "@microsoft/1ds-core-js@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-4.0.1.tgz#6fcf129276c933934d618f7fc3de49613259e3ec" - integrity sha512-BcTKhcRixCqLYEo1xVlQofvvZpgey9qmkzxQd1j9bBZS4iQl9IOmGBzlf/vGSITC7VXtEk2NZOlHQN0llAIfSw== +"@microsoft/1ds-core-js@3.2.13", "@microsoft/1ds-core-js@^3.2.13": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz#0c105ed75091bae3f1555c0334704fa9911c58fb" + integrity sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg== dependencies: - "@microsoft/applicationinsights-core-js" "3.0.2" - "@microsoft/applicationinsights-shims" "^3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.6 < 2.x" + "@microsoft/applicationinsights-core-js" "2.8.15" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/1ds-post-js@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-4.0.1.tgz#8630927d10518067686104baec98557b74317e4e" - integrity sha512-uyYkG9mxzzj/4HgxVJsgcz9abfakW+Rjrsnor/hXS5I5ezE3YUX+54DLvAh/c6mEgoDI7u8wGkGl3HgCtAYbDQ== +"@microsoft/1ds-post-js@^3.2.13": + version "3.2.13" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz#560aacac8a92fdbb79e8c2ebcb293d56e19f51aa" + integrity sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA== dependencies: - "@microsoft/1ds-core-js" "4.0.1" - "@microsoft/applicationinsights-shims" "^3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.6 < 2.x" + "@microsoft/1ds-core-js" "3.2.13" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/applicationinsights-core-js@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz#108e20df8c162bec92b1f66f9de2530a25d9f51a" - integrity sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ== +"@microsoft/applicationinsights-core-js@2.8.15": + version "2.8.15" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz#8fa466474260e01967fe649f14dd9e5ff91dcdc8" + integrity sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ== dependencies: - "@microsoft/applicationinsights-shims" "3.0.1" - "@microsoft/dynamicproto-js" "^2.0.2" - "@nevware21/ts-async" ">= 0.2.4 < 2.x" - "@nevware21/ts-utils" ">= 0.9.5 < 2.x" + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.9" -"@microsoft/applicationinsights-shims@3.0.1", "@microsoft/applicationinsights-shims@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz#3865b73ace8405b9c4618cc5c571f2fe3876f06f" - integrity sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg== - dependencies: - "@nevware21/ts-utils" ">= 0.9.4 < 2.x" - -"@microsoft/dynamicproto-js@^2.0.2": +"@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2": version "2.0.2" - resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz#e57fbec2e7067d48b7e8e1e1c1d354028ef718a6" - integrity sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg== - dependencies: - "@nevware21/ts-utils" ">= 0.9.4 < 2.x" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" + integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== + +"@microsoft/dynamicproto-js@^1.1.7", "@microsoft/dynamicproto-js@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6" + integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ== "@mole-inc/bin-wrapper@^8.0.1": version "8.0.1" @@ -534,18 +524,6 @@ got "^11.8.5" os-filter-obj "^2.0.0" -"@nevware21/ts-async@>= 0.2.4 < 2.x": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@nevware21/ts-async/-/ts-async-0.2.6.tgz#9fad1c4a56b01430b407d454817f68bb6ace92b0" - integrity sha512-NCUqEZSbsy7LVtKlUScd/eTst6djkWauLlzoIPVKCOxalEBdO8lrgNRIm4Xy68JNudNN5faqa2WA12X8m0BVhA== - dependencies: - "@nevware21/ts-utils" ">= 0.9.7 < 2.x" - -"@nevware21/ts-utils@>= 0.9.4 < 2.x", "@nevware21/ts-utils@>= 0.9.5 < 2.x", "@nevware21/ts-utils@>= 0.9.6 < 2.x", "@nevware21/ts-utils@>= 0.9.7 < 2.x": - version "0.9.8" - resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.9.8.tgz#6db23211a5c7539d3b4362e273087effcfc7d2f8" - integrity sha512-kZ8s8hcn9jPVX/M7kSsBYrOGlHjqLahmxrG7QeKTk5paeVwfgKdvVCjj5Acb4UGb/ukU1G34U1Z3eb7bbVanyA== - "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -3609,10 +3587,10 @@ electron-to-chromium@^1.4.202: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.207.tgz#9c3310ebace2952903d05dcaba8abe3a4ed44c01" integrity sha512-piH7MJDJp4rJCduWbVvmUd59AUne1AFBJ8JaRQvk0KzNTSUnZrVXHCZc+eg+CGE4OujkcLJznhGKD6tuAshj5Q== -electron@25.4.0: - version "25.4.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-25.4.0.tgz#d45b1cf3e4e96eb5bff5fee704d7aa13b532f3a5" - integrity sha512-VLTRxDhL4UvQbqM7pTNENnJo62cdAPZT92N+B7BZQ5Xfok1wuVPEewIjBot4K7U3EpLUuHn1veeLzho3ihiP+Q== +electron@25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-25.5.0.tgz#6465d49c0731424e3e48776628c35771697caf11" + integrity sha512-w1DNj1LuAk0Vaas1rQ0pAkTe2gZ5YG75J27mC2m88y0G6Do5b5YoFDaF84fOGQHeQ4j8tC5LngSgWhbwmqDlrw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18" @@ -10097,10 +10075,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.2.0-dev.20230807: - version "5.2.0-dev.20230807" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.0-dev.20230807.tgz#652fa8a3fcc46958035d9b911c815299deb9f58e" - integrity sha512-sF8sZl3r/mpAdKAxASaWaoU+mNPF3g8OrZ601HraU2l4WEwAf6nO7sq0Bzl+Y3FV7sWjy+gb6kdM1CtLjVne/g== +typescript@^5.3.0-dev.20230816: + version "5.3.0-dev.20230816" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.0-dev.20230816.tgz#409982c629164811db1eb62b365ed2e1b526458d" + integrity sha512-iEOudrx61DsbJn+z2bVX+/FldF7ILAuGwQYO2EvF4F33Q8DUV0KSkiikxUB83VVH8ExkwQHVNdtkr16wd2V71w== typical@^4.0.0: version "4.0.0" @@ -10781,45 +10759,45 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-canvas@0.5.0-beta.9: - version "0.5.0-beta.9" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.9.tgz#d4a9536cf586f78a54527751e03abf6445613886" - integrity sha512-AMyFctHbIWx0aLcACINuZqFFNoz4ndGtIAb4pQguIl9t8r+2otiV11MY4FJmPM0kUzEzoLWzVFJaGxOzykCWuw== +xterm-addon-canvas@0.5.0-beta.22: + version "0.5.0-beta.22" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0-beta.22.tgz#513f0c2b7cf96073f47627b27e8965c1b1a22431" + integrity sha512-9F6ZI0DMRgffVfHkLkDwl5n8VscvCaV10tWI3skXOX7Y7Aws6OEeglkOPoU3IllofCU792kHKM4pPoToUxTltg== -xterm-addon-image@0.6.0-beta.1: - version "0.6.0-beta.1" - resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.1.tgz#058c171a4866f1cde7608e3b00050476b55ece4c" - integrity sha512-vLxUEo/KAsEaRhMshJ4uxnfc/Si8YfT3kilaK5WuiJflVODzQjytzZrl2iLeZaWKiqQp0NR6AEhuXlYASK675A== +xterm-addon-image@0.6.0-beta.14: + version "0.6.0-beta.14" + resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.6.0-beta.14.tgz#75fc3f824123183a4bbb5306e22f8b2c6966b0a6" + integrity sha512-D5Gh5JTKhHaPt1KwQNf6diF37KA4eToJw3XId1wy62tWmSqfq+QflhOGTfd+SnSQYCktU05ETzM+0tncIU62pQ== -xterm-addon-search@0.13.0-beta.7: - version "0.13.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.7.tgz#2f9bf9d8380c4c40f021c47dc583aa32bea8796e" - integrity sha512-Dnuws0ThaqzxRDo8TjkSb5WlMYycONVDSr/irx2pk2I4vC5RuSpOifmD46ALlryNSDkMasjfCLjHPILHsFDCfw== +xterm-addon-search@0.13.0-beta.20: + version "0.13.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0-beta.20.tgz#8ddd0513e2a70fcefa325722100d2e1bfaf3b9cb" + integrity sha512-wrx6187cJ1UenGL6ZeYv3jFvRPhhENTfbC+Hv1Fnww8LmsKhcj+0+Pm6yInNjX/9hNVsNzdqKyqNeEMoykyoyA== -xterm-addon-serialize@0.11.0-beta.7: - version "0.11.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.11.0-beta.7.tgz#934a48a4490c2ca94a5cfcdd646fb0813b4e56d0" - integrity sha512-TwYsEHq6TflY0vmq3yAI2VKNqzEAZ0gCm3TThDMPrPQiv4LcewQSeGu9RujEqF9RAhujhceaM3GajBxySAWyqQ== +xterm-addon-serialize@0.11.0-beta.20: + version "0.11.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.11.0-beta.20.tgz#e879b34d214761403f1081833f9221c6903bf0c3" + integrity sha512-OXnC1SATaz7kEFjFWhyv9MJaXi8yHdPjazpGLNi11h33CRTKtCQiqqPBHU87dztnXmpEX6Jw0/jr3zlyXuAmnw== -xterm-addon-unicode11@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0.tgz#41c0d96acc1e3bb6c6596eee64e163b6bca74be7" - integrity sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg== +xterm-addon-unicode11@0.6.0-beta.12: + version "0.6.0-beta.12" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.6.0-beta.12.tgz#ac6df9d635325dc692e4c602e74a2fc27a09405c" + integrity sha512-9wWWf/5nFafYgq0pn9EgAWnXaXGleVxfjNOqavpLRYFv0nw42QbaYyGvnGcxyYHM5Aqx/8rYE/DDVWZBqQZdYA== -xterm-addon-webgl@0.16.0-beta.13: - version "0.16.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.13.tgz#ffacd22b8d86ca7f9ba903a64bc85654523955fb" - integrity sha512-1avJBoAKl+0dOjRK0pvu2im6gaXyP/6oJcEBQXCTz3V8YlgcG/jom8A6FEQSEDGZ/4dnyDvnLbsg3+aLZIr6gw== +xterm-addon-webgl@0.16.0-beta.30: + version "0.16.0-beta.30" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.30.tgz#820d5c65f868b14ec4177bfb8a294931a53616bf" + integrity sha512-39qPHPFmNENxcHf8/CzGHS6wzKMMegoRkHB1+scqtBhSxFaD8tX5Ye33HZIEdQ9nXe9xtr4FWVp77T+n9hdrew== -xterm-headless@5.3.0-beta.39: - version "5.3.0-beta.39" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.39.tgz#84a509dbe1dc9b2229b5274ae3430ce968986346" - integrity sha512-Q4vfqAaYoWh7/T4HearlLlKkn9vjN5EugsNyuPd6hq+Ug1FwJIJYj+I9vY67+Rqxr24XGnrQdXfvvEas29KXsw== +xterm-headless@5.3.0-beta.61: + version "5.3.0-beta.61" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.61.tgz#28654550cb572709b99ea3eb8672d4568ae141c9" + integrity sha512-yfkbPLUtKjE4K7DsZ204A1BuOKpu6Usqi6rIYWT4XRMi+LjnkTbBjGr2BSjyJ3Gmtm+cSgBD0SvRN+V3xNxbxA== -xterm@5.3.0-beta.39: - version "5.3.0-beta.39" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.39.tgz#b64da3fd1b374fd535db61294a49c16453dbb77e" - integrity sha512-Z1r0ijTgad5MjFoz8qZ+2Qc3bgjG6kYY/kZI3DTA1pibArHeOMDYk9+IeCb9adF74zvZurjytRfXKJgX6M9hwg== +xterm@5.3.0-beta.61: + version "5.3.0-beta.61" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.61.tgz#a6c27d90a5314da51d80deeb32f3bd77f1e1c8f6" + integrity sha512-rJHpCc48GSpHnu0SSERynQ80D5ikvFVsqhv6JdmeONTrnAFRr134OglJRIpbi2YK8UPbV6F6Dfqm/AQh+9GZzA== y18n@^3.2.1: version "3.2.2"