diff --git a/.rebase/CHANGELOG.md b/.rebase/CHANGELOG.md index 285f3e253ff..4fa7e8630cf 100644 --- a/.rebase/CHANGELOG.md +++ b/.rebase/CHANGELOG.md @@ -11,7 +11,6 @@ https://github.com/che-incubator/che-code/pull/331 #### @vitaliy-guliy https://github.com/che-incubator/che-code/pull/328 -- code/extensions/che-api/src/extension.ts - code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts --- @@ -34,3 +33,16 @@ https://github.com/che-incubator/che-code/commit/eed0a5213ba1b29b810d53f6365aaa2 - code/src/vs/server/webClientServer.ts --- + +#### @vitaliy-guliy +https://github.com/che-incubator/che-code/pull/211 + +- code/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +- code/src/vs/workbench/contrib/webview/browser/pre/index.html +--- + +#### @RomanNikitenko +https://github.com/che-incubator/che-code/pull/193 + +- code/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +--- diff --git a/code/.gitignore b/code/.gitignore index c0459c86043..123b3567059 100644 --- a/code/.gitignore +++ b/code/.gitignore @@ -4,6 +4,7 @@ npm-debug.log Thumbs.db node_modules/ .build/ +.vscode/extensions/**/out/ extensions/**/dist/ /out*/ /extensions/**/out/ diff --git a/code/.nvmrc b/code/.nvmrc index 4a1f488b6c3..a9d087399d7 100644 --- a/code/.nvmrc +++ b/code/.nvmrc @@ -1 +1 @@ -18.17.1 +18.19.0 diff --git a/code/.vscode/notebooks/api.github-issues b/code/.vscode/notebooks/api.github-issues index 2a6f3ec1bc5..6b8a385ec42 100644 --- a/code/.vscode/notebooks/api.github-issues +++ b/code/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"February 2024\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"March 2024\"" }, { "kind": 1, diff --git a/code/.vscode/notebooks/endgame.github-issues b/code/.vscode/notebooks/endgame.github-issues index ee1084be56d..750e53e4b26 100644 --- a/code/.vscode/notebooks/endgame.github-issues +++ b/code/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2024\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"March 2024\"" }, { "kind": 1, diff --git a/code/.vscode/notebooks/my-endgame.github-issues b/code/.vscode/notebooks/my-endgame.github-issues index a286082c738..ab59f23283f 100644 --- a/code/.vscode/notebooks/my-endgame.github-issues +++ b/code/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2024\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"March 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/code/.vscode/notebooks/my-work.github-issues b/code/.vscode/notebooks/my-work.github-issues index 2a3f9159703..b23dacf87e4 100644 --- a/code/.vscode/notebooks/my-work.github-issues +++ b/code/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"February 2024\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"March 2024\"\n" }, { "kind": 1, diff --git a/code/.vscode/settings.json b/code/.vscode/settings.json index ba7c0f4dd2b..121972d6ec6 100644 --- a/code/.vscode/settings.json +++ b/code/.vscode/settings.json @@ -170,6 +170,5 @@ }, "css.format.spaceAroundSelectorSeparator": true, "inlineChat.mode": "live", - "testing.defaultGutterClickAction": "contextMenu", "typescript.enablePromptUseWorkspaceTsdk": true, } diff --git a/code/.yarnrc b/code/.yarnrc index 19c5cb1eb8f..05efa60c84c 100644 --- a/code/.yarnrc +++ b/code/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "27.3.2" -ms_build_id "26836302" +target "28.2.6" +ms_build_id "27476517" runtime "electron" build_from_source "true" diff --git a/code/SECURITY.md b/code/SECURITY.md index 4fa5946a867..82db58aa7c8 100644 --- a/code/SECURITY.md +++ b/code/SECURITY.md @@ -1,34 +1,34 @@ - + ## Security -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: -* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) -* Full paths of source file(s) related to the manifestation of the issue -* The location of the affected source code (tag/branch/commit or direct URL) -* Any special configuration required to reproduce the issue -* Step-by-step instructions to reproduce the issue -* Proof-of-concept or exploit code (if possible) -* Impact of the issue, including how an attacker might exploit the issue + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. ## Preferred Languages @@ -36,6 +36,6 @@ We prefer all communications to be in English. ## Policy -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). diff --git a/code/build/.cachesalt b/code/build/.cachesalt index 89591977f28..8051d84124e 100644 --- a/code/build/.cachesalt +++ b/code/build/.cachesalt @@ -1 +1 @@ -2024-02-05T09:34:15.476Z +2024-03-18T08:47:22.277Z diff --git a/code/build/azure-pipelines/common/publish.js b/code/build/azure-pipelines/common/publish.js index e6b24921ac1..e2f3a2e0e6f 100644 --- a/code/build/azure-pipelines/common/publish.js +++ b/code/build/azure-pipelines/common/publish.js @@ -370,7 +370,7 @@ async function unzip(packagePath, outputPath) { }); } // Contains all of the logic for mapping details to our actual product names in CosmosDB -function getPlatform(product, os, arch, type) { +function getPlatform(product, os, arch, type, isLegacy) { switch (os) { case 'win32': switch (product) { @@ -421,9 +421,12 @@ function getPlatform(product, os, arch, type) { case 'client': return `linux-${arch}`; case 'server': - return `server-linux-${arch}`; + return isLegacy ? `legacy-server-linux-${arch}` : `server-linux-${arch}`; case 'web': - return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + if (arch === 'standalone') { + return 'web-standalone'; + } + return isLegacy ? `legacy-server-linux-${arch}-web` : `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -476,7 +479,7 @@ function getRealType(type) { } async function processArtifact(artifact, artifactFilePath) { const log = (...args) => console.log(`[${artifact.name}]`, ...args); - const match = /^vscode_(?[^_]+)_(?[^_]+)_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); + const match = /^vscode(?:_legacy)?_(?[^_]+)_(?[^_]+)_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { throw new Error(`Invalid artifact name: ${artifact.name}`); } @@ -484,7 +487,8 @@ async function processArtifact(artifact, artifactFilePath) { const quality = e('VSCODE_QUALITY'); const commit = e('BUILD_SOURCEVERSION'); const { product, os, arch, unprocessedType } = match.groups; - const platform = getPlatform(product, os, arch, unprocessedType); + const isLegacy = artifact.name.includes('_legacy'); + const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); const type = getRealType(unprocessedType); const size = fs.statSync(artifactFilePath).size; const stream = fs.createReadStream(artifactFilePath); @@ -595,7 +599,7 @@ async function main() { operations.push({ name: artifact.name, operation }); resultPromise = Promise.allSettled(operations.map(o => o.operation)); } - await new Promise(c => setTimeout(c, 10000)); + await new Promise(c => setTimeout(c, 10_000)); } console.log(`Found all ${done.size + processing.size} artifacts, waiting for ${processing.size} artifacts to finish publishing...`); const artifactsInProgress = operations.filter(o => processing.has(o.name)); diff --git a/code/build/azure-pipelines/common/publish.ts b/code/build/azure-pipelines/common/publish.ts index f144a7be793..bd9b0cb1037 100644 --- a/code/build/azure-pipelines/common/publish.ts +++ b/code/build/azure-pipelines/common/publish.ts @@ -531,7 +531,7 @@ interface Asset { } // Contains all of the logic for mapping details to our actual product names in CosmosDB -function getPlatform(product: string, os: string, arch: string, type: string): string { +function getPlatform(product: string, os: string, arch: string, type: string, isLegacy: boolean): string { switch (os) { case 'win32': switch (product) { @@ -582,9 +582,12 @@ function getPlatform(product: string, os: string, arch: string, type: string): s case 'client': return `linux-${arch}`; case 'server': - return `server-linux-${arch}`; + return isLegacy ? `legacy-server-linux-${arch}` : `server-linux-${arch}`; case 'web': - return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + if (arch === 'standalone') { + return 'web-standalone'; + } + return isLegacy ? `legacy-server-linux-${arch}-web` : `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -639,7 +642,7 @@ function getRealType(type: string) { async function processArtifact(artifact: Artifact, artifactFilePath: string): Promise { const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args); - const match = /^vscode_(?[^_]+)_(?[^_]+)_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); + const match = /^vscode(?:_legacy)?_(?[^_]+)_(?[^_]+)_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { throw new Error(`Invalid artifact name: ${artifact.name}`); @@ -649,7 +652,8 @@ async function processArtifact(artifact: Artifact, artifactFilePath: string): Pr const quality = e('VSCODE_QUALITY'); const commit = e('BUILD_SOURCEVERSION'); const { product, os, arch, unprocessedType } = match.groups!; - const platform = getPlatform(product, os, arch, unprocessedType); + const isLegacy = artifact.name.includes('_legacy'); + const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); const type = getRealType(unprocessedType); const size = fs.statSync(artifactFilePath).size; const stream = fs.createReadStream(artifactFilePath); diff --git a/code/build/azure-pipelines/common/retry.js b/code/build/azure-pipelines/common/retry.js index 7b90b0cac5b..91f60bf24b2 100644 --- a/code/build/azure-pipelines/common/retry.js +++ b/code/build/azure-pipelines/common/retry.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.retry = void 0; +exports.retry = retry; async function retry(fn) { let lastError; for (let run = 1; run <= 10; run++) { @@ -24,5 +24,4 @@ async function retry(fn) { console.error(`Too many retries, aborting.`); throw lastError; } -exports.retry = retry; //# sourceMappingURL=retry.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/common/sign.js b/code/build/azure-pipelines/common/sign.js index 4dba4765ff6..32996a7db03 100644 --- a/code/build/azure-pipelines/common/sign.js +++ b/code/build/azure-pipelines/common/sign.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.main = exports.Temp = void 0; +exports.Temp = void 0; +exports.main = main; const cp = require("child_process"); const fs = require("fs"); const crypto = require("crypto"); @@ -164,7 +165,6 @@ function main([esrpCliPath, type, cert, username, password, folderPath, pattern] process.exit(1); } } -exports.main = main; if (require.main === module) { main(process.argv.slice(2)); process.exit(0); diff --git a/code/build/azure-pipelines/linux/install.sh b/code/build/azure-pipelines/linux/install.sh deleted file mode 100755 index 57f58763cca..00000000000 --- a/code/build/azure-pipelines/linux/install.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# To workaround the issue of yarn not respecting the registry value from .npmrc -yarn config set registry "$NPM_REGISTRY" - -SYSROOT_ARCH=$VSCODE_ARCH -if [ "$SYSROOT_ARCH" == "x64" ]; then - SYSROOT_ARCH="amd64" -fi - -export VSCODE_SYSROOT_DIR=$PWD/.build/sysroots -SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' - -if [ "$npm_config_arch" == "x64" ]; then - # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/118.0.5993.159/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux - - # Download libcxx headers and objects from upstream electron releases - DEBUG=libcxx-fetcher \ - VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ - VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ - VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ - VSCODE_ARCH="$npm_config_arch" \ - node build/linux/libcxx-fetcher.js - - # Set compiler toolchain - # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/118.0.5993.159:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/118.0.5993.159:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/118.0.5993.159:build/config/c++/BUILD.gn - export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" - export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" -elif [ "$npm_config_arch" == "arm64" ]; then - # Set compiler toolchain for client native modules and remote server - export CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc - export CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" -elif [ "$npm_config_arch" == "arm" ]; then - # Set compiler toolchain for client native modules and remote server - export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" -fi - -for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." -done diff --git a/code/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/code/build/azure-pipelines/linux/product-build-linux-legacy-server.yml new file mode 100644 index 00000000000..80e6a33e6d1 --- /dev/null +++ b/code/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -0,0 +1,222 @@ +parameters: + - name: VSCODE_QUALITY + type: string + - name: VSCODE_RUN_INTEGRATION_TESTS + type: boolean + - name: VSCODE_ARCH + type: string + +steps: + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + + - template: ../distro/download-distro.yml + + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + + - script: tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output + + - script: | + set -e + # Start X server + sudo apt-get update + sudo apt-get install -y pkg-config \ + dbus \ + xvfb \ + libgtk-3-0 \ + libxkbfile-dev \ + libkrb5-dev \ + libgbm1 \ + rpm + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + # Start dbus session + sudo mkdir -p /var/run/dbus + DBUS_LAUNCH_RESULT=$(sudo dbus-daemon --config-file=/usr/share/dbus-1/system.conf --print-address) + echo "##vso[task.setvariable variable=DBUS_SESSION_BUS_ADDRESS]$DBUS_LAUNCH_RESULT" + displayName: Setup system services + + - script: node build/setup-npm-registry.js $NPM_REGISTRY + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Registry + + - script: | + set -e + npm config set registry "$NPM_REGISTRY" --location=project + # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb + # following is a workaround for yarn to send authorization header + # for GET requests to the registry. + echo "always-auth=true" >> .npmrc + yarn config set registry "$NPM_REGISTRY" + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM & Yarn + + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Authentication + + - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: + - task: Docker@1 + displayName: "Pull Docker image" + inputs: + azureSubscriptionEndpoint: "vscode-builds-subscription" + azureContainerRegistry: vscodehub.azurecr.io + command: "Run an image" + imageName: vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) + containerCommand: uname + + - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: + - task: Docker@1 + displayName: "Pull Docker image" + inputs: + azureSubscriptionEndpoint: "vscode-builds-subscription" + azureContainerRegistry: vscodehub.azurecr.io + command: "Run an image" + imageName: vscode-linux-build-agent:bionic-arm32v7 + containerCommand: uname + + - script: | + set -e + # To workaround the issue of yarn not respecting the registry value from .npmrc + yarn config set registry "$NPM_REGISTRY" + + for i in {1..5}; do # try 5 times + yarn --cwd build --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done + + export VSCODE_SYSROOT_PREFIX='-glibc-2.17' + source ./build/azure-pipelines/linux/setup-env.sh --only-remote + + for i in {1..5}; do # try 5 times + yarn --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done + env: + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + NPM_REGISTRY: "$(NPM_REGISTRY)" + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" + ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: + VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) + ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: + VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-arm32v7 + displayName: Install dependencies + + - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: + - script: | + set -e + EXPECTED_GLIBC_VERSION="2.17" \ + EXPECTED_GLIBCXX_VERSION="3.4.19" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules + + - script: node build/azure-pipelines/distro/mixin-npm + displayName: Mixin distro node modules + + - script: node build/azure-pipelines/distro/mixin-quality + displayName: Mixin distro quality + + - template: ../common/install-builtin-extensions.yml + + - script: | + set -e + yarn gulp vscode-linux-$(VSCODE_ARCH)-min-ci + ARCHIVE_PATH=".build/linux/client/code-${{ parameters.VSCODE_QUALITY }}-$(VSCODE_ARCH)-$(date +%s).tar.gz" + mkdir -p $(dirname $ARCHIVE_PATH) + echo "##vso[task.setvariable variable=CLIENT_PATH]$ARCHIVE_PATH" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Build client + + - script: | + set -e + tar -czf $CLIENT_PATH -C .. VSCode-linux-$(VSCODE_ARCH) + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Archive client + + - script: | + set -e + export VSCODE_NODE_GLIBC="-glibc-2.17" + yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci + mv ../vscode-reh-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH) # TODO@joaomoreno + ARCHIVE_PATH=".build/linux/server/vscode-server-linux-$(VSCODE_ARCH).tar.gz" + mkdir -p $(dirname $ARCHIVE_PATH) + tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) + echo "##vso[task.setvariable variable=LEGACY_SERVER_PATH]$ARCHIVE_PATH" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Build server + + - script: | + set -e + export VSCODE_NODE_GLIBC="-glibc-2.17" + yarn gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci + mv ../vscode-reh-web-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH)-web # TODO@joaomoreno + ARCHIVE_PATH=".build/linux/web/vscode-server-linux-$(VSCODE_ARCH)-web.tar.gz" + mkdir -p $(dirname $ARCHIVE_PATH) + tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH)-web + echo "##vso[task.setvariable variable=LEGACY_WEB_PATH]$ARCHIVE_PATH" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Build server (web) + + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - template: product-build-linux-test.yml + parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} + VSCODE_RUN_SMOKE_TESTS: false + + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: Generate SBOM (server) + inputs: + BuildComponentPath: $(Build.SourcesDirectory)/remote + BuildDropPath: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) + PackageName: Legacy Visual Studio Code Server + + - publish: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)/_manifest + displayName: Publish SBOM (server) + artifact: $(ARTIFACT_PREFIX)sbom_vscode_legacy_server_linux_$(VSCODE_ARCH) + + - publish: $(LEGACY_SERVER_PATH) + artifact: $(ARTIFACT_PREFIX)vscode_legacy_server_linux_$(VSCODE_ARCH)_archive-unsigned + condition: and(succeededOrFailed(), ne(variables['LEGACY_SERVER_PATH'], '')) + displayName: Publish server archive + + - publish: $(LEGACY_WEB_PATH) + artifact: $(ARTIFACT_PREFIX)vscode_legacy_web_linux_$(VSCODE_ARCH)_archive-unsigned + condition: and(succeededOrFailed(), ne(variables['LEGACY_WEB_PATH'], '')) + displayName: Publish web server archive diff --git a/code/build/azure-pipelines/linux/product-build-linux.yml b/code/build/azure-pipelines/linux/product-build-linux.yml index e4b4fab899b..a85cf60cf5e 100644 --- a/code/build/azure-pipelines/linux/product-build-linux.yml +++ b/code/build/azure-pipelines/linux/product-build-linux.yml @@ -103,6 +103,8 @@ steps: - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | set -e + # To workaround the issue of yarn not respecting the registry value from .npmrc + yarn config set registry "$NPM_REGISTRY" for i in {1..5}; do # try 5 times yarn --cwd build --frozen-lockfile --check-files && break @@ -113,7 +115,16 @@ steps: echo "Yarn failed $i, trying again..." done - ./build/azure-pipelines/linux/install.sh + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + yarn --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done env: npm_config_arch: $(NPM_ARCH) VSCODE_ARCH: $(VSCODE_ARCH) @@ -121,23 +132,17 @@ steps: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" - VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: - VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-arm32v7 displayName: Install dependencies (non-OSS) condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - script: | - set -e + - script: | + set -e - EXPECTED_GLIBC_VERSION="2.17" \ - EXPECTED_GLIBCXX_VERSION="3.4.19" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.25" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - script: node build/azure-pipelines/distro/mixin-npm condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/code/build/azure-pipelines/linux/setup-env.sh b/code/build/azure-pipelines/linux/setup-env.sh new file mode 100755 index 00000000000..e42a6b12b1f --- /dev/null +++ b/code/build/azure-pipelines/linux/setup-env.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +set -e + +SYSROOT_ARCH=$VSCODE_ARCH +if [ "$SYSROOT_ARCH" == "x64" ]; then + SYSROOT_ARCH="amd64" +fi + +export VSCODE_SYSROOT_DIR=$PWD/.build/sysroots +SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' + +if [ "$npm_config_arch" == "x64" ]; then + if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then + # Download clang based on chromium revision used by vscode + curl -s https://raw.githubusercontent.com/chromium/chromium/120.0.6099.268/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + + # Download libcxx headers and objects from upstream electron releases + DEBUG=libcxx-fetcher \ + VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ + VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ + VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ + VSCODE_ARCH="$npm_config_arch" \ + node build/linux/libcxx-fetcher.js + + # Set compiler toolchain + # Flags for the client build are based on + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/c++/BUILD.gn + export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" + export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" + + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu" + fi +elif [ "$npm_config_arch" == "arm64" ]; then + if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" + + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" + fi +elif [ "$npm_config_arch" == "arm" ]; then + if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" + + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" + fi +fi diff --git a/code/build/azure-pipelines/product-build.yml b/code/build/azure-pipelines/product-build.yml index 54a37e9b1fa..d529b159c48 100644 --- a/code/build/azure-pipelines/product-build.yml +++ b/code/build/azure-pipelines/product-build.yml @@ -40,14 +40,26 @@ parameters: displayName: "🎯 Linux x64" type: boolean default: true + - name: VSCODE_BUILD_LINUX_X64_LEGACY_SERVER + displayName: "🎯 Linux x64 Legacy Server" + type: boolean + default: true - name: VSCODE_BUILD_LINUX_ARM64 displayName: "🎯 Linux arm64" type: boolean default: true + - name: VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER + displayName: "🎯 Linux arm64 Legacy Server" + type: boolean + default: true - name: VSCODE_BUILD_LINUX_ARMHF displayName: "🎯 Linux armhf" type: boolean default: true + - name: VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER + displayName: "🎯 Linux armhf Legacy Server" + type: boolean + default: true - name: VSCODE_BUILD_ALPINE displayName: "🎯 Alpine x64" type: boolean @@ -102,6 +114,8 @@ variables: value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_LINUX value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }} + - name: VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER + value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX_X64_LEGACY_SERVER, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER, true)) }} - name: VSCODE_BUILD_STAGE_ALPINE value: ${{ or(eq(parameters.VSCODE_BUILD_ALPINE, true), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_MACOS @@ -445,6 +459,49 @@ stages: VSCODE_RUN_INTEGRATION_TESTS: false VSCODE_RUN_SMOKE_TESTS: false + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER'], true)) }}: + - stage: LinuxLegacyServer + dependsOn: + - Compile + pool: 1es-ubuntu-20.04-x64 + jobs: + - ${{ if eq(parameters.VSCODE_BUILD_LINUX_X64_LEGACY_SERVER, true) }}: + - job: Linuxx64LegacyServer + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux-legacy-server.yml + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + + - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER, true) }}: + - job: LinuxArmhfLegacyServer + variables: + VSCODE_ARCH: armhf + NPM_ARCH: arm + steps: + - template: linux/product-build-linux-legacy-server.yml + parameters: + VSCODE_ARCH: armhf + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_INTEGRATION_TESTS: false + + - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER, true) }}: + - job: LinuxArm64LegacyServer + variables: + VSCODE_ARCH: arm64 + NPM_ARCH: arm64 + steps: + - template: linux/product-build-linux-legacy-server.yml + parameters: + VSCODE_ARCH: arm64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_INTEGRATION_TESTS: false + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_ALPINE'], true)) }}: - stage: Alpine dependsOn: diff --git a/code/build/checksums/electron.txt b/code/build/checksums/electron.txt index a774dffc830..b77f73fb763 100644 --- a/code/build/checksums/electron.txt +++ b/code/build/checksums/electron.txt @@ -1,75 +1,75 @@ -032e54843700736bf3566518ff88717b2dc70be41bdc43840993fcb4cd9c82e8 *chromedriver-v27.3.2-darwin-arm64.zip -7d693267bacc510b724b97db23e21e22983e9f500605a132ab519303ec2e4d94 *chromedriver-v27.3.2-darwin-x64.zip -5f3f417986667e4c82c492b30c14892b0fef3a6dcf07860e74f7d7ba29f0ca41 *chromedriver-v27.3.2-linux-arm64.zip -84364d9c1fc53ce6f29e41d08d12351a2a4a208646acf02551c6f9aa6029c163 *chromedriver-v27.3.2-linux-armv7l.zip -7d3965a5ca3217e16739153d2817fc292e7cb16f55034fde76f26bdc916e60d1 *chromedriver-v27.3.2-linux-x64.zip -068adc1ea9e1d21dcfef1468b2b789714c93465c1874dbd3bf2872a695a1279f *chromedriver-v27.3.2-mas-arm64.zip -0d4d4bb8971260cbc0058cab2a7972e556b83a19d6ea062ea226e7a8555bc369 *chromedriver-v27.3.2-mas-x64.zip -83ffc61b6b524ee0caa0e5cd02dcd00adcd166ba1e03e7fc50206a299a6fca11 *chromedriver-v27.3.2-win32-arm64.zip -df4e9f20681b3e7b65c41dd1df3aa8cb9bc0a061a24ddcffbe44a9191aa01e0c *chromedriver-v27.3.2-win32-ia32.zip -1ef67b7c06061e691176df5e3463f4d5f5f258946dac24ae62e3cc250b8b95d1 *chromedriver-v27.3.2-win32-x64.zip -f3c52d205572da71a23f436b4708dc89c721a74f0e0c5c51093e3e331b1dff67 *electron-api.json -1489dca88c89f6fef05bdc2c08b9623bb46eb8d0f43020985776daef08642061 *electron-v27.3.2-darwin-arm64-dsym-snapshot.zip -7ee895e81d695c1ed65378ff4514d4fc9c4015a1c3c67691765f92c08c8e0855 *electron-v27.3.2-darwin-arm64-dsym.zip -cbc1c9973b2a895aa2ebecdbd92b3fe8964590b12141a658a6d03ed97339fae6 *electron-v27.3.2-darwin-arm64-symbols.zip -0d4efeff14ac16744eef3d461b95fb59abd2c3affbf638af169698135db73e1f *electron-v27.3.2-darwin-arm64.zip -a77b52509213e67ae1e24172256479831ecbff55d1f49dc0e8bfd4818a5f393e *electron-v27.3.2-darwin-x64-dsym-snapshot.zip -9006386321c50aa7e0e02cd9bd9daef4b8c3ec0e9735912524802f31d02399ef *electron-v27.3.2-darwin-x64-dsym.zip -14fa8e76e519e1fb9e166e134d03f3df1ae1951c14dfd76db8a033a9627c0f13 *electron-v27.3.2-darwin-x64-symbols.zip -5105acce7d832a606fd11b0551d1ef00e0c49fc8b4cff4b53712c9efdddc27a2 *electron-v27.3.2-darwin-x64.zip -3bc20fb4f1d5effb2d882e7b587a337f910026aa50c22e7bc92522daa13f389c *electron-v27.3.2-linux-arm64-debug.zip -0d5d97a93938fa62d2659e2053dcc8d1cabc967878992b248bfec4dcc7763b8c *electron-v27.3.2-linux-arm64-symbols.zip -db9320d9ec6309145347fbba369ab7634139e80f15fff452be9b0171b2bd1823 *electron-v27.3.2-linux-arm64.zip -3bc20fb4f1d5effb2d882e7b587a337f910026aa50c22e7bc92522daa13f389c *electron-v27.3.2-linux-armv7l-debug.zip -6b9117419568c72542ab671301df05d46a662deab0bc37787b3dc9a907e68f8c *electron-v27.3.2-linux-armv7l-symbols.zip -72fd10c666dd810e9f961c2727ae44f5f6cf964cedb6860c1f09da7152e29a29 *electron-v27.3.2-linux-armv7l.zip -354209d48be01785d286eb80d691cdff476479db2d8cdbc6b6bd30652f5539fa *electron-v27.3.2-linux-x64-debug.zip -5f45a4b42f3b35ecea8a623338a6add35bb5220cb0ed02e3489b6d77fbe102ef *electron-v27.3.2-linux-x64-symbols.zip -2261aa0a5a293cf963487c050e9f6d05124da1f946f99bd1115f616f8730f286 *electron-v27.3.2-linux-x64.zip -54a4ad6e75e5a0001c32de18dbfec17f5edc17693663078076456ded525d65da *electron-v27.3.2-mas-arm64-dsym-snapshot.zip -5a5c85833ad7a6ef04337ed8acd131e5cf383a49638789dfd84e07c855b33ccc *electron-v27.3.2-mas-arm64-dsym.zip -16da4cc5a19a953c839093698f0532854e4d3fc839496a5c2b2405fd63c707f4 *electron-v27.3.2-mas-arm64-symbols.zip -8455b79826fe195124bee3f0661e08c14ca50858d376b09d03c79aace0082ea5 *electron-v27.3.2-mas-arm64.zip -00731db08a1bb66e51af0d26d03f8510221f4f6f92282c7baa0cd1c130e0cce6 *electron-v27.3.2-mas-x64-dsym-snapshot.zip -446f98f2d957e4ae487a6307b18be7b11edff35187b71143def4d00325943e42 *electron-v27.3.2-mas-x64-dsym.zip -d3455394eff02d463fdf89aabeee9c05d4980207ecf75a5eac27b35fb2aef874 *electron-v27.3.2-mas-x64-symbols.zip -dae434f52ff9b1055703aaf74b17ff3d93351646e9271a3b10e14b49969d4218 *electron-v27.3.2-mas-x64.zip -a598fcd1e20dcef9e7dccf7676ba276cd95ec7ff6799834fd090800fb15a6507 *electron-v27.3.2-win32-arm64-pdb.zip -7ba64940321ddff307910cc49077aa36c430d4b0797097975cb797cc0ab2b39d *electron-v27.3.2-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v27.3.2-win32-arm64-toolchain-profile.zip -692f264e9d13478ad9a42d06e2eead0ed67ab1b52fc3693ba536a6a441fd9010 *electron-v27.3.2-win32-arm64.zip -a74eee739ff26681f6696f7959ab8e8603bb57f8fcb7ddab305220f71d2c69f3 *electron-v27.3.2-win32-ia32-pdb.zip -c10b90b51d0292129dc5bba5e012c7e07c78d6c70b0980c36676d6abf8eef12f *electron-v27.3.2-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v27.3.2-win32-ia32-toolchain-profile.zip -63e477332608d31afb965a4054b5d78165df1da65d57477ac1dbddf8ede0f1b9 *electron-v27.3.2-win32-ia32.zip -3d795150c0afd48f585c7d32685f726618825262cb76f4014567be9e3de88732 *electron-v27.3.2-win32-x64-pdb.zip -d5463f797d1eb9a57ac9b20caa6419c15c5f3b378a3cb2b45d338040d7124411 *electron-v27.3.2-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v27.3.2-win32-x64-toolchain-profile.zip -e701b3023d4929f86736ae8a7ff6409134455da99b3fbdcea8d58555acbd9d46 *electron-v27.3.2-win32-x64.zip -3383cd44951cf763ddd36ba3ec91c930c9e8d33a175adfcb6dce4f667d60bc34 *electron.d.ts -db6df7bd0264c859009247276b35eda4ef20f22a7b2f41c2335a4609f5653cb7 *ffmpeg-v27.3.2-darwin-arm64.zip -3c0bb9740d6b95ff476ff7a5d4b442ccef7ec98e0fa3f2bad8d0e6a51329b511 *ffmpeg-v27.3.2-darwin-x64.zip -6fea38ce22bae4d23fb6b143e946c1c3d214ccecabf841883a2cb1b621161113 *ffmpeg-v27.3.2-linux-arm64.zip -926d0da25ffcea3d05a6cbcae15e5d7729d93bc43394ae4439747669d2210e1d *ffmpeg-v27.3.2-linux-armv7l.zip -6f9c0ef52af14828ad547a80b17f8c63cac51a18b8d5769a2f33e4fa6cccfc7e *ffmpeg-v27.3.2-linux-x64.zip -c75f62fc08d6c5e49fd1a805ca00b4191d5f04d26469448e3d4af48fb409b3a7 *ffmpeg-v27.3.2-mas-arm64.zip -acb8154c113ecbafb91aef5a294dc2c2bce61cbc4a261696681b723d292a5cb3 *ffmpeg-v27.3.2-mas-x64.zip -1665bdac6aa7264a6eb5f00a93110b718c7231010389bdda5ec7bf8275aab953 *ffmpeg-v27.3.2-win32-arm64.zip -3972d89c60a77f7955d7e8520adeae0c9f449a5ae3730cacf202f2baf2bae079 *ffmpeg-v27.3.2-win32-ia32.zip -37d2da723c2f2148c1c8f2ccf354b6dd933148c49dfc7f32aa57ecbd7063ffaf *ffmpeg-v27.3.2-win32-x64.zip -8828099c931c56981865fb9ff6fca85012dd05702a125858d6377c793760db1f *hunspell_dictionaries.zip -9e2126db472f66d3dde2d77eec63364e7071358f5591fc3c4dfb53d191ab5da8 *libcxx-objects-v27.3.2-linux-arm64.zip -530c3a92c4cd721e49e62d4fd97090c4e4d1b00c3ba821fd4f42c5f9186c98e7 *libcxx-objects-v27.3.2-linux-armv7l.zip -5b67f5e2a268bd1980a13b794013d4ac96e7ee40c4878d96f7c27da2c3f94923 *libcxx-objects-v27.3.2-linux-x64.zip -0d3086ccf9a050a88251a4382349f436f99d3d2b1842d87d854ea80667f6c423 *libcxx_headers.zip -ac02262548cb396051c683ad35fcbbed61b9a6f935c2a2bd3d568b209ce9e5a4 *libcxxabi_headers.zip -ba3b63a297b8be954a0ca1b8b83c3c856abaae85d17e6337d2b34e1c14f0d4b2 *mksnapshot-v27.3.2-darwin-arm64.zip -cb09a9e9e1fee567bf9e697eef30d143bd30627c0b189d0271cf84a72a03042e *mksnapshot-v27.3.2-darwin-x64.zip -014c5b621bbbc497bdc40dac47fac20143013fa1e905c0570b5cf92a51826354 *mksnapshot-v27.3.2-linux-arm64-x64.zip -f71407b9cc5c727de243a9e9e7fb56d2a0880e02187fa79982478853432ed5b7 *mksnapshot-v27.3.2-linux-armv7l-x64.zip -e5caa81f467d071756a4209f05f360055be7625a71a0dd9b2a8c95296c8415b5 *mksnapshot-v27.3.2-linux-x64.zip -fc33ec02a17fb58d48625c7b68517705dcd95b5a12e731d0072711a084dc65bd *mksnapshot-v27.3.2-mas-arm64.zip -961af5fc0ef80243d0e94036fb31b90f7e8458e392dd0e49613c11be89cb723f *mksnapshot-v27.3.2-mas-x64.zip -844a70ccef160921e0baeaefe9038d564db9a9476d98fab1eebb5c122ba9c22c *mksnapshot-v27.3.2-win32-arm64-x64.zip -3e723ca42794d43e16656599fbfec73880b964264f5057e38b865688c83ac905 *mksnapshot-v27.3.2-win32-ia32.zip -3e6fc056fa8cfb9940b26c4f066a9c9343056f053bcc53e1eada464bf5bc0d42 *mksnapshot-v27.3.2-win32-x64.zip +2cd042f38fd13cbb3ed0e7205c6c892cd5f04fd4992d18da363b8f0df9dda3eb *chromedriver-v28.2.6-darwin-arm64.zip +05bc772ecb5728cde1efed2308074ad53a4abfe7c541a82d6fef62d3350c6cf4 *chromedriver-v28.2.6-darwin-x64.zip +4c7ea31be89009fcedfe8e3619be61bec6056c8bb9ea93b4e6a5deec791f8c55 *chromedriver-v28.2.6-linux-arm64.zip +ae61e86c512dff5108f2240018c3b549b57e25f3f31e822effb7f1d5a53cd474 *chromedriver-v28.2.6-linux-armv7l.zip +d2eac837adf3691abfab267d5e5f2428450c3ca506d74e47382bd0ae73755a4a *chromedriver-v28.2.6-linux-x64.zip +326f6f4ce44e42bae98894eb3f3ef125fe887a1188ce98d8cc1e8b68862283fe *chromedriver-v28.2.6-mas-arm64.zip +4cb08690d4db116f115e5da2f2d9ed9ccb287a33a8c9cb7264dba1329117f979 *chromedriver-v28.2.6-mas-x64.zip +ce1124ac3e5b91efc78d95260e5ecb001b362f12f1c9d2abc71fc3e8140aefb1 *chromedriver-v28.2.6-win32-arm64.zip +1a36b630b828953873a102c118d7954409de7ae0e40bdcb325baca0915fde4ff *chromedriver-v28.2.6-win32-ia32.zip +7e138e53e1acada2047c9adda42ee3760397cda56f7c73f30b48f69c51fb136f *chromedriver-v28.2.6-win32-x64.zip +f8809dc99407cc14bdc6579a6205d391ecf285a6d9ef49a34d529371616cd032 *electron-api.json +3bd369be1ce7175a637eb5531317c49c13287152cae4e0cfb875acdceee92fe3 *electron-v28.2.6-darwin-arm64-dsym-snapshot.zip +0020309287b4eef7cc59b761a1d604af80cca6d195cfecea5b97b834ba808d2a *electron-v28.2.6-darwin-arm64-dsym.zip +b1aeb1b30a965cf439456beaa3e99228437f3f9f91ddbbfa27a1695143a8a892 *electron-v28.2.6-darwin-arm64-symbols.zip +432ef2d5767991347c9452961e392182baa761d0b8b23483c1117a8c75bf18e9 *electron-v28.2.6-darwin-arm64.zip +c4723e680bf78ebe7e4a151d0b68c8e698985c36007237e9c5ffbd3976451519 *electron-v28.2.6-darwin-x64-dsym-snapshot.zip +86845958cedf3af045f07fd287066678e0ff73a8caf29c8032e8def0d3277b23 *electron-v28.2.6-darwin-x64-dsym.zip +450f7324fb9b0baed557133af50c8772a4b3e33f1288a7e732f7cc8fbd9df30d *electron-v28.2.6-darwin-x64-symbols.zip +524d710d21d64b539e568946debb3659b8e8071ead56c4b1a598c7c76fc32089 *electron-v28.2.6-darwin-x64.zip +c6ecf165f51d7da20278324a7454cc5119e6e546527dc9f21e7d4701f062443d *electron-v28.2.6-linux-arm64-debug.zip +cb495ec65c3a5cb6639a2ff1110f588cc82df241982e5cbb91932990de723772 *electron-v28.2.6-linux-arm64-symbols.zip +cdc832c6e337a2241bec78b7130f21c6db01d90d0ef93cd3c934f220319fa696 *electron-v28.2.6-linux-arm64.zip +c6ecf165f51d7da20278324a7454cc5119e6e546527dc9f21e7d4701f062443d *electron-v28.2.6-linux-armv7l-debug.zip +9eb155513a0a6f6fa518c2c768e0cc483a3e35c7beb7b657211df7bcf33ad144 *electron-v28.2.6-linux-armv7l-symbols.zip +6e340b9468950d8d3b5a9bf7840622403346f043af3f25471beff32212d227ce *electron-v28.2.6-linux-armv7l.zip +51567b886d0510726e733d9ecb33f32f14c78ee8cdedfa56adae26c0ac59b890 *electron-v28.2.6-linux-x64-debug.zip +fd6e2bc61e6df6113a74503d60bdea23d6734ba75bc270fa87f2a99a472d2e22 *electron-v28.2.6-linux-x64-symbols.zip +a5ec62621ccd0cd4636dc290a0406abc706c2900b518b085bff2312a5ee1dc6f *electron-v28.2.6-linux-x64.zip +1fb074339d42ef399254199418849f0fd591ba6bb203ab0570be192d7225921a *electron-v28.2.6-mas-arm64-dsym-snapshot.zip +3a8228698d1a85103eb3958de0ba8d77f1129a4eca44227a46dc70eda3ce2abc *electron-v28.2.6-mas-arm64-dsym.zip +3668d2aa7d00679f93106b1feb1dab4f1284bf5c6a041aa47284693786b3ad08 *electron-v28.2.6-mas-arm64-symbols.zip +ba40b18e6964fa96a72a86984032b534b94596b5a29418d286fba090f6ec8076 *electron-v28.2.6-mas-arm64.zip +dc7d071fb39d89c65745f6a567011959c4cd32e60e95cb92d970bc0ca89da26f *electron-v28.2.6-mas-x64-dsym-snapshot.zip +812909c73e1ebcb121e7874ae2250ed55ce58e3ef651fcfac9bd92284b1f6d69 *electron-v28.2.6-mas-x64-dsym.zip +fd720ee5353c20ff1ff0dc1b8eeaa64f28f7860268d5f8528d468ced0375086b *electron-v28.2.6-mas-x64-symbols.zip +9ce774a52e32a7df11c6ca20ae766303a33f1fd9000c628238fe93426b73216e *electron-v28.2.6-mas-x64.zip +952360b9cc257c145de62111cf9f0569892b3dfde3d4f8246b4025d8931c0377 *electron-v28.2.6-win32-arm64-pdb.zip +095500f4db01a8448cf7263a9db053446d88c08e1f6abd9a84323b7c45bd5a25 *electron-v28.2.6-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.6-win32-arm64-toolchain-profile.zip +99b24366555381bdaf35e4b85956941c859afbbfc52b5cc66bbda7ace4bcdc26 *electron-v28.2.6-win32-arm64.zip +85f92b7d9f5689c92216c71b3e76a3e1181f3b74b1a30649c5870126d197c057 *electron-v28.2.6-win32-ia32-pdb.zip +276b143933a186e397820424fddc6d0488d3293828e76273d64a6d642b64b67d *electron-v28.2.6-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.6-win32-ia32-toolchain-profile.zip +8f4547b567c5e88b7b5c08381d9da21210cbf796cf4b8348f2f15c139d03dc3a *electron-v28.2.6-win32-ia32.zip +1e875caf77e8ba4f622743e015522b1bef6b73eacebbfb00b9f62cd1fd46a3d3 *electron-v28.2.6-win32-x64-pdb.zip +c57843add2a3106247c3e16b5a246bfb43a046a114826f222004d72c54ab6e0b *electron-v28.2.6-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.6-win32-x64-toolchain-profile.zip +cc27e8af85c8cde97cc53204b612365f3b1c53215e19bb5b6f303ea9491b4953 *electron-v28.2.6-win32-x64.zip +be5b134abac3cb2f771246712a564080b2e63475fe9f09accc7acb6ade03af3f *electron.d.ts +767539ad20af8cda91da9bf35183ddaea7a09aa3ee8274d2677f407502f24295 *ffmpeg-v28.2.6-darwin-arm64.zip +af8422c1596adf13887cac74aa185d3c84787174af305a49e558664162c0bbed *ffmpeg-v28.2.6-darwin-x64.zip +8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.6-linux-arm64.zip +51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.6-linux-armv7l.zip +acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.6-linux-x64.zip +d478d239203f337f146ba2d6f5af6640a82a8591faac23017f8709e0fbb61d8a *ffmpeg-v28.2.6-mas-arm64.zip +77bb31ee80979dce6b1ee786ab39eba7dd56dbdf29101e7046ee8cea1938e350 *ffmpeg-v28.2.6-mas-x64.zip +fc406f7a4239d5c37d4dbc44907184213b7e07de9d39796cbef7eaa4ead92549 *ffmpeg-v28.2.6-win32-arm64.zip +abd92844333712e2a2a891b2679cbaf434daf7aa50c371bfccccd553d2394300 *ffmpeg-v28.2.6-win32-ia32.zip +c5ce83bdfeb037f315bea8c97bfae344ebbec255fd173a7a769fd276b2bdbf28 *ffmpeg-v28.2.6-win32-x64.zip +cc27058b50af2fe95070f52aa72e417f27f440cac2ae0471f31af061181272fd *hunspell_dictionaries.zip +b593c7f79c5fd49794dcf260ebd8e5b757313b467d3f671c5d2422f7ed3829f2 *libcxx-objects-v28.2.6-linux-arm64.zip +92a9f593ccb41c5507c0be01f1ec061d4090290e45c9b6aa003070b4b8fcb839 *libcxx-objects-v28.2.6-linux-armv7l.zip +fce1088e2bbc3bbcacae1741c2f7f2508ddf0e00f41450ff96d83df655ee431e *libcxx-objects-v28.2.6-linux-x64.zip +ee7ad0db6eb01ee72a70bc6ecf27428d1fd31ab52329fb75aa2b2a9702b1c1d7 *libcxx_headers.zip +1a2473c8e94c23a2c00a580c1ae379e2e74cae89ccf9dae977ceb9ba44658801 *libcxxabi_headers.zip +9365c442b6bbce858814028fed11a79a518e64d433d01821264afdf5492f9308 *mksnapshot-v28.2.6-darwin-arm64.zip +8410f72e3696691cb38ea31acfc6d501291df43bd20636059aaafbef9dc7757d *mksnapshot-v28.2.6-darwin-x64.zip +e081547def25c9af1f1ace4aa3ed0ee175bae7c401c92ee42988981d605cb8a7 *mksnapshot-v28.2.6-linux-arm64-x64.zip +c6b2d51a82fd10a04532f33385c78921dd85722a9fe3de107ab4809df08ae0e8 *mksnapshot-v28.2.6-linux-armv7l-x64.zip +dc80bc57f86e361e0769e1ed62b1d083c0dd1d160c6fcb2c10ae821a83ae6333 *mksnapshot-v28.2.6-linux-x64.zip +9ff56ed57c48df1e449c334bbe61874bd495d92978924565fdd2ec99e5be7a54 *mksnapshot-v28.2.6-mas-arm64.zip +fb04465058adad0e56f7c3dfa6d9d85b249554595925669e7454348e28490e02 *mksnapshot-v28.2.6-mas-x64.zip +681633334d9eaab61ab95c3718ba98e7bf339615b6189d51b0782b77f5e8eaea *mksnapshot-v28.2.6-win32-arm64-x64.zip +d3986690b413d43cc6a003f8e09fd07bf213eb5f006f3419e9615fcc09d4891d *mksnapshot-v28.2.6-win32-ia32.zip +b26942469d96148f7ec410996d9e4c34c3c013a441e568b05c87e36a3d9ae441 *mksnapshot-v28.2.6-win32-x64.zip diff --git a/code/build/checksums/nodejs.txt b/code/build/checksums/nodejs.txt index 9ed8af5842a..13aa4c7e87b 100644 --- a/code/build/checksums/nodejs.txt +++ b/code/build/checksums/nodejs.txt @@ -1,6 +1,6 @@ -18ca716ea57522b90473777cb9f878467f77fdf826d37beb15a0889fdd74533e node-v18.17.1-darwin-arm64.tar.gz -b3e083d2715f07ec3f00438401fb58faa1e0bdf3c7bde9f38b75ed17809d92fa node-v18.17.1-darwin-x64.tar.gz -8f5203f5c6dc44ea50ac918b7ecbdb1c418e4f3d9376d8232a1ef9ff38f9c480 node-v18.17.1-linux-arm64.tar.gz -1ab79868859b2d37148c6d8ecee3abb5ee55b88731ab5df01928ed4f6f9bfbad node-v18.17.1-linux-armv7l.tar.gz -2cb75f2bc04b0a3498733fbee779b2f76fe3f655188b4ac69ef2887b6721da2d node-v18.17.1-linux-x64.tar.gz -afb45186ad4f4217c2fc1dfc2239ff5ab016ef0ba5fc329bc6aa8fd10c7ecc88 win-x64/node.exe +9f982cc91b28778dd8638e4f94563b0c2a1da7aba62beb72bd427721035ab553 node-v18.18.2-darwin-arm64.tar.gz +5bb8da908ed590e256a69bf2862238c8a67bc4600119f2f7721ca18a7c810c0f node-v18.18.2-darwin-x64.tar.gz +0c9a6502b66310cb26e12615b57304e91d92ac03d4adcb91c1906351d7928f0d node-v18.18.2-linux-arm64.tar.gz +7a3b34a6fdb9514bc2374114ec6df3c36113dc5075c38b22763aa8f106783737 node-v18.18.2-linux-armv7l.tar.gz +a44c3e7f8bf91e852c928e5d8bd67ca316b35e27eec1d8acbe3b9dbe03688dab node-v18.18.2-linux-x64.tar.gz +54884183ff5108874c091746465e8156ae0acc68af589cc10bc41b3927db0f4a win-x64/node.exe diff --git a/code/build/gulpfile.extensions.js b/code/build/gulpfile.extensions.js index 9080ba79b3f..2d40e40f6d0 100644 --- a/code/build/gulpfile.extensions.js +++ b/code/build/gulpfile.extensions.js @@ -22,83 +22,79 @@ const commit = getVersion(root); const plumber = require('gulp-plumber'); const ext = require('./lib/extensions'); -const extensionsPath = path.join(path.dirname(__dirname), 'extensions'); - // To save 250ms for each gulp startup, we are caching the result here // const compilations = glob.sync('**/tsconfig.json', { // cwd: extensionsPath, // ignore: ['**/out/**', '**/node_modules/**'] // }); const compilations = [ - 'authentication-proxy/tsconfig.json', - 'che-activity-tracker/tsconfig.json', - 'che-api/tsconfig.json', - 'che-commands/tsconfig.json', - 'che-port/tsconfig.json', - 'che-remote/tsconfig.json', - 'che-resource-monitor/tsconfig.json', - 'che-terminal/tsconfig.json', - 'che-telemetry/tsconfig.json', - 'che-github-authentication/tsconfig.json', - 'configuration-editing/build/tsconfig.json', - 'configuration-editing/tsconfig.json', - 'css-language-features/client/tsconfig.json', - 'css-language-features/server/tsconfig.json', - 'debug-auto-launch/tsconfig.json', - 'debug-server-ready/tsconfig.json', - 'emmet/tsconfig.json', - 'extension-editing/tsconfig.json', - 'git/tsconfig.json', - 'git-base/tsconfig.json', - 'github-authentication/tsconfig.json', - 'github/tsconfig.json', - 'grunt/tsconfig.json', - 'gulp/tsconfig.json', - 'html-language-features/client/tsconfig.json', - 'html-language-features/server/tsconfig.json', - 'ipynb/tsconfig.json', - 'jake/tsconfig.json', - 'json-language-features/client/tsconfig.json', - 'json-language-features/server/tsconfig.json', - 'markdown-language-features/preview-src/tsconfig.json', - 'markdown-language-features/server/tsconfig.json', - 'markdown-language-features/tsconfig.json', - 'markdown-math/tsconfig.json', - 'media-preview/tsconfig.json', - 'merge-conflict/tsconfig.json', - 'microsoft-authentication/tsconfig.json', - 'notebook-renderers/tsconfig.json', - 'npm/tsconfig.json', - 'php-language-features/tsconfig.json', - 'search-result/tsconfig.json', - 'references-view/tsconfig.json', - 'simple-browser/tsconfig.json', - 'tunnel-forwarding/tsconfig.json', - 'typescript-language-features/test-workspace/tsconfig.json', - 'typescript-language-features/web/tsconfig.json', - 'typescript-language-features/tsconfig.json', - 'vscode-api-tests/tsconfig.json', - 'vscode-colorize-tests/tsconfig.json', - 'vscode-test-resolver/tsconfig.json' + 'extensions/che-activity-tracker/tsconfig.json', + 'extensions/che-api/tsconfig.json', + 'extensions/che-commands/tsconfig.json', + 'extensions/che-port/tsconfig.json', + 'extensions/che-remote/tsconfig.json', + 'extensions/che-resource-monitor/tsconfig.json', + 'extensions/che-terminal/tsconfig.json', + 'extensions/che-telemetry/tsconfig.json', + 'extensions/che-github-authentication/tsconfig.json', + 'extensions/configuration-editing/tsconfig.json', + 'extensions/css-language-features/client/tsconfig.json', + 'extensions/css-language-features/server/tsconfig.json', + 'extensions/debug-auto-launch/tsconfig.json', + 'extensions/debug-server-ready/tsconfig.json', + 'extensions/emmet/tsconfig.json', + 'extensions/extension-editing/tsconfig.json', + 'extensions/git/tsconfig.json', + 'extensions/git-base/tsconfig.json', + 'extensions/github/tsconfig.json', + 'extensions/github-authentication/tsconfig.json', + 'extensions/grunt/tsconfig.json', + 'extensions/gulp/tsconfig.json', + 'extensions/html-language-features/client/tsconfig.json', + 'extensions/html-language-features/server/tsconfig.json', + 'extensions/ipynb/tsconfig.json', + 'extensions/jake/tsconfig.json', + 'extensions/json-language-features/client/tsconfig.json', + 'extensions/json-language-features/server/tsconfig.json', + 'extensions/markdown-language-features/preview-src/tsconfig.json', + 'extensions/markdown-language-features/server/tsconfig.json', + 'extensions/markdown-language-features/tsconfig.json', + 'extensions/markdown-math/tsconfig.json', + 'extensions/media-preview/tsconfig.json', + 'extensions/merge-conflict/tsconfig.json', + 'extensions/microsoft-authentication/tsconfig.json', + 'extensions/notebook-renderers/tsconfig.json', + 'extensions/npm/tsconfig.json', + 'extensions/php-language-features/tsconfig.json', + 'extensions/references-view/tsconfig.json', + 'extensions/search-result/tsconfig.json', + 'extensions/simple-browser/tsconfig.json', + 'extensions/tunnel-forwarding/tsconfig.json', + 'extensions/typescript-language-features/test-workspace/tsconfig.json', + 'extensions/typescript-language-features/web/tsconfig.json', + 'extensions/typescript-language-features/tsconfig.json', + 'extensions/vscode-api-tests/tsconfig.json', + 'extensions/vscode-colorize-tests/tsconfig.json', + 'extensions/vscode-test-resolver/tsconfig.json' ]; const getBaseUrl = out => `https://ticino.blob.core.windows.net/sourcemaps/${commit}/${out}`; const tasks = compilations.map(function (tsconfigFile) { - const absolutePath = path.join(extensionsPath, tsconfigFile); - const relativeDirname = path.dirname(tsconfigFile); + const absolutePath = path.join(root, tsconfigFile); + const relativeDirname = path.dirname(tsconfigFile.replace(/^(.*\/)?extensions\//i, '')); const overrideOptions = {}; overrideOptions.sourceMap = true; const name = relativeDirname.replace(/\//g, '-'); - const root = path.join('extensions', relativeDirname); - const srcBase = path.join(root, 'src'); + const srcRoot = path.dirname(tsconfigFile); + const srcBase = path.join(srcRoot, 'src'); const src = path.join(srcBase, '**'); - const srcOpts = { cwd: path.dirname(__dirname), base: srcBase }; + const srcOpts = { cwd: root, base: srcBase, dot: true }; - const out = path.join(root, 'out'); + const out = path.join(srcRoot, 'out'); const baseUrl = getBaseUrl(out); let headerId, headerOut; @@ -125,7 +121,7 @@ const tasks = compilations.map(function (tsconfigFile) { const pipeline = function () { const input = es.through(); - const tsFilter = filter(['**/*.ts', '!**/lib/lib*.d.ts', '!**/node_modules/**'], { restore: true }); + const tsFilter = filter(['**/*.ts', '!**/lib/lib*.d.ts', '!**/node_modules/**'], { restore: true, dot: true }); const output = input .pipe(plumber({ errorHandler: function (err) { @@ -149,7 +145,7 @@ const tasks = compilations.map(function (tsconfigFile) { .pipe(tsFilter.restore) .pipe(build ? nlsDev.bundleMetaDataFiles(headerId, headerOut) : es.through()) // Filter out *.nls.json file. We needed them only to bundle meta data file. - .pipe(filter(['**', '!**/*.nls.json'])) + .pipe(filter(['**', '!**/*.nls.json'], { dot: true })) .pipe(reporter.end(emitError)); return es.duplex(input, output); @@ -281,6 +277,7 @@ exports.watchWebExtensionsTask = watchWebExtensionsTask; * @param {boolean} isWatch */ async function buildWebExtensions(isWatch) { + const extensionsPath = path.join(root, 'extensions'); const webpackConfigLocations = await nodeUtil.promisify(glob)( path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), { ignore: ['**/node_modules'] } diff --git a/code/build/gulpfile.reh.js b/code/build/gulpfile.reh.js index 8a9cdf84691..bae1f6380f0 100644 --- a/code/build/gulpfile.reh.js +++ b/code/build/gulpfile.reh.js @@ -379,6 +379,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa if (platform === 'linux' || platform === 'alpine') { result = es.merge(result, gulp.src(`resources/server/bin/helpers/check-requirements-linux.sh`, { base: '.' }) + .pipe(replace('@@SERVER_APPLICATION_NAME@@', product.serverApplicationName)) .pipe(rename(`bin/helpers/check-requirements.sh`)) .pipe(util.setExecutableBit()) ); diff --git a/code/build/lib/asar.js b/code/build/lib/asar.js index cadb9ab974d..31845f2f2dd 100644 --- a/code/build/lib/asar.js +++ b/code/build/lib/asar.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAsar = void 0; +exports.createAsar = createAsar; const path = require("path"); const es = require("event-stream"); const pickle = require('chromium-pickle-js'); @@ -115,5 +115,4 @@ function createAsar(folderPath, unpackGlobs, destFilename) { } }); } -exports.createAsar = createAsar; //# sourceMappingURL=asar.js.map \ No newline at end of file diff --git a/code/build/lib/builtInExtensions.js b/code/build/lib/builtInExtensions.js index 1b0adc48d4c..463ce16e18d 100644 --- a/code/build/lib/builtInExtensions.js +++ b/code/build/lib/builtInExtensions.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getBuiltInExtensions = exports.getExtensionStream = void 0; +exports.getExtensionStream = getExtensionStream; +exports.getBuiltInExtensions = getBuiltInExtensions; const fs = require("fs"); const path = require("path"); const os = require("os"); @@ -58,7 +59,6 @@ function getExtensionStream(extension) { } return getExtensionDownloadStream(extension); } -exports.getExtensionStream = getExtensionStream; function syncMarketplaceExtension(extension) { const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl; const source = ansiColors.blue(galleryServiceUrl ? '[marketplace]' : '[github]'); @@ -127,7 +127,6 @@ function getBuiltInExtensions() { .on('end', resolve); }); } -exports.getBuiltInExtensions = getBuiltInExtensions; if (require.main === module) { getBuiltInExtensions().then(() => process.exit(0)).catch(err => { console.error(err); diff --git a/code/build/lib/bundle.js b/code/build/lib/bundle.js index 5d3ee9d5118..61d9f015624 100644 --- a/code/build/lib/bundle.js +++ b/code/build/lib/bundle.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.bundle = void 0; +exports.bundle = bundle; const fs = require("fs"); const path = require("path"); const vm = require("vm"); @@ -78,7 +78,6 @@ function bundle(entryPoints, config, callback) { }); }, (err) => callback(err, null)); } -exports.bundle = bundle; function emitEntryPoints(modules, entryPoints) { const modulesMap = {}; modules.forEach((m) => { diff --git a/code/build/lib/compilation.js b/code/build/lib/compilation.js index 35bc464d34a..85cd722dbf3 100644 --- a/code/build/lib/compilation.js +++ b/code/build/lib/compilation.js @@ -4,7 +4,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = exports.watchTask = exports.compileTask = exports.transpileTask = void 0; +exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = void 0; +exports.transpileTask = transpileTask; +exports.compileTask = compileTask; +exports.watchTask = watchTask; const es = require("event-stream"); const fs = require("fs"); const gulp = require("gulp"); @@ -96,10 +99,9 @@ function transpileTask(src, out, swc) { task.taskName = `transpile-${path.basename(src)}`; return task; } -exports.transpileTask = transpileTask; function compileTask(src, out, build, options = {}) { const task = () => { - if (os.totalmem() < 4000000000) { + if (os.totalmem() < 4_000_000_000) { throw new Error('compilation requires 4GB of RAM'); } const compile = createCompile(src, build, true, false); @@ -137,7 +139,6 @@ function compileTask(src, out, build, options = {}) { task.taskName = `compile-${path.basename(src)}`; return task; } -exports.compileTask = compileTask; function watchTask(out, build) { const task = () => { const compile = createCompile('src', build, false, false); @@ -153,7 +154,6 @@ function watchTask(out, build) { task.taskName = `watch-${path.basename(out)}`; return task; } -exports.watchTask = watchTask; const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); class MonacoGenerator { _isWatch; diff --git a/code/build/lib/dependencies.js b/code/build/lib/dependencies.js index 64087a9ac17..1f2dd75d68c 100644 --- a/code/build/lib/dependencies.js +++ b/code/build/lib/dependencies.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getProductionDependencies = void 0; +exports.getProductionDependencies = getProductionDependencies; const fs = require("fs"); const path = require("path"); const cp = require("child_process"); @@ -69,7 +69,6 @@ function getProductionDependencies(folderPath) { } return [...new Set(result)]; } -exports.getProductionDependencies = getProductionDependencies; if (require.main === module) { console.log(JSON.stringify(getProductionDependencies(root), null, ' ')); } diff --git a/code/build/lib/extensions.js b/code/build/lib/extensions.js index c81568c7275..6a6c0a7b4cd 100644 --- a/code/build/lib/extensions.js +++ b/code/build/lib/extensions.js @@ -4,7 +4,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildExtensionMedia = exports.webpackExtensions = exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromGithub = exports.fromMarketplace = void 0; +exports.fromMarketplace = fromMarketplace; +exports.fromGithub = fromGithub; +exports.packageLocalExtensionsStream = packageLocalExtensionsStream; +exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; +exports.scanBuiltinExtensions = scanBuiltinExtensions; +exports.translatePackageJSON = translatePackageJSON; +exports.webpackExtensions = webpackExtensions; +exports.buildExtensionMedia = buildExtensionMedia; const es = require("event-stream"); const fs = require("fs"); const cp = require("child_process"); @@ -213,7 +220,6 @@ function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, met .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } -exports.fromMarketplace = fromMarketplace; function fromGithub({ name, version, repo, sha256, metadata }) { const json = require('gulp-json-editor'); fancyLog('Downloading extension from GH:', ansiColors.yellow(`${name}@${version}`), '...'); @@ -232,7 +238,6 @@ function fromGithub({ name, version, repo, sha256, metadata }) { .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } -exports.fromGithub = fromGithub; const excludedExtensions = [ 'vscode-api-tests', 'vscode-colorize-tests', @@ -306,7 +311,6 @@ function packageLocalExtensionsStream(forWeb, disableMangle) { return (result .pipe(util2.setExecutableBit(['**/*.sh']))); } -exports.packageLocalExtensionsStream = packageLocalExtensionsStream; function packageMarketplaceExtensionsStream(forWeb) { const marketplaceExtensionsDescriptions = [ ...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)), @@ -325,7 +329,6 @@ function packageMarketplaceExtensionsStream(forWeb) { return (marketplaceExtensionsStream .pipe(util2.setExecutableBit(['**/*.sh']))); } -exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; function scanBuiltinExtensions(extensionsRoot, exclude = []) { const scannedExtensions = []; try { @@ -361,7 +364,6 @@ function scanBuiltinExtensions(extensionsRoot, exclude = []) { return scannedExtensions; } } -exports.scanBuiltinExtensions = scanBuiltinExtensions; function translatePackageJSON(packageJSON, packageNLSPath) { const CharCode_PC = '%'.charCodeAt(0); const packageNls = JSON.parse(fs.readFileSync(packageNLSPath).toString()); @@ -385,7 +387,6 @@ function translatePackageJSON(packageJSON, packageNLSPath) { translate(packageJSON); return packageJSON; } -exports.translatePackageJSON = translatePackageJSON; const extensionsPath = path.join(root, 'extensions'); // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ @@ -459,7 +460,6 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { } }); } -exports.webpackExtensions = webpackExtensions; async function esbuildExtensions(taskName, isWatch, scripts) { function reporter(stdError, script) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); @@ -500,5 +500,4 @@ async function buildExtensionMedia(isWatch, outputRoot) { outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined }))); } -exports.buildExtensionMedia = buildExtensionMedia; //# sourceMappingURL=extensions.js.map \ No newline at end of file diff --git a/code/build/lib/fetch.js b/code/build/lib/fetch.js index ba23e78257c..2fed63bca0e 100644 --- a/code/build/lib/fetch.js +++ b/code/build/lib/fetch.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.fetchGithub = exports.fetchUrl = exports.fetchUrls = void 0; +exports.fetchUrls = fetchUrls; +exports.fetchUrl = fetchUrl; +exports.fetchGithub = fetchGithub; const es = require("event-stream"); const VinylFile = require("vinyl"); const log = require("fancy-log"); @@ -30,7 +32,6 @@ function fetchUrls(urls, options) { }); })); } -exports.fetchUrls = fetchUrls; async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { const verbose = !!options.verbose ?? (!!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']); try { @@ -94,7 +95,6 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { throw e; } } -exports.fetchUrl = fetchUrl; const ghApiHeaders = { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'VSCode Build', @@ -135,5 +135,4 @@ function fetchGithub(repo, options) { } })); } -exports.fetchGithub = fetchGithub; //# sourceMappingURL=fetch.js.map \ No newline at end of file diff --git a/code/build/lib/getVersion.js b/code/build/lib/getVersion.js index abf05e93210..b50ead538a2 100644 --- a/code/build/lib/getVersion.js +++ b/code/build/lib/getVersion.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVersion = void 0; +exports.getVersion = getVersion; const git = require("./git"); function getVersion(root) { let version = process.env['BUILD_SOURCEVERSION']; @@ -13,5 +13,4 @@ function getVersion(root) { } return version; } -exports.getVersion = getVersion; //# sourceMappingURL=getVersion.js.map \ No newline at end of file diff --git a/code/build/lib/git.js b/code/build/lib/git.js index a8e712ed070..798a408bdb9 100644 --- a/code/build/lib/git.js +++ b/code/build/lib/git.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVersion = void 0; +exports.getVersion = getVersion; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. @@ -51,5 +51,4 @@ function getVersion(repo) { } return refs[ref]; } -exports.getVersion = getVersion; //# sourceMappingURL=git.js.map \ No newline at end of file diff --git a/code/build/lib/i18n.js b/code/build/lib/i18n.js index eaf624dc154..960b360915a 100644 --- a/code/build/lib/i18n.js +++ b/code/build/lib/i18n.js @@ -4,7 +4,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.EXTERNAL_EXTENSIONS = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.XLF = exports.Line = exports.extraLanguages = exports.defaultLanguages = void 0; +exports.EXTERNAL_EXTENSIONS = exports.XLF = exports.Line = exports.extraLanguages = exports.defaultLanguages = void 0; +exports.processNlsFiles = processNlsFiles; +exports.getResource = getResource; +exports.createXlfFilesForCoreBundle = createXlfFilesForCoreBundle; +exports.createXlfFilesForExtensions = createXlfFilesForExtensions; +exports.createXlfFilesForIsl = createXlfFilesForIsl; +exports.prepareI18nPackFiles = prepareI18nPackFiles; +exports.prepareIslFiles = prepareIslFiles; const path = require("path"); const fs = require("fs"); const event_stream_1 = require("event-stream"); @@ -423,7 +430,6 @@ function processNlsFiles(opts) { this.queue(file); }); } -exports.processNlsFiles = processNlsFiles; const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench', extensionsProject = 'vscode-extensions', setupProject = 'vscode-setup', serverProject = 'vscode-server'; function getResource(sourceFile) { let resource; @@ -458,7 +464,6 @@ function getResource(sourceFile) { } throw new Error(`Could not identify the XLF bundle for ${sourceFile}`); } -exports.getResource = getResource; function createXlfFilesForCoreBundle() { return (0, event_stream_1.through)(function (file) { const basename = path.basename(file.path); @@ -506,7 +511,6 @@ function createXlfFilesForCoreBundle() { } }); } -exports.createXlfFilesForCoreBundle = createXlfFilesForCoreBundle; function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder) { const prefix = prefixWithBuildFolder ? '.build/' : ''; return gulp @@ -653,7 +657,6 @@ function createXlfFilesForExtensions() { } }); } -exports.createXlfFilesForExtensions = createXlfFilesForExtensions; function createXlfFilesForIsl() { return (0, event_stream_1.through)(function (file) { let projectName, resourceFile; @@ -704,7 +707,6 @@ function createXlfFilesForIsl() { this.queue(xlfFile); }); } -exports.createXlfFilesForIsl = createXlfFilesForIsl; function createI18nFile(name, messages) { const result = Object.create(null); result[''] = [ @@ -793,7 +795,6 @@ function prepareI18nPackFiles(resultingTranslationPaths) { }); }); } -exports.prepareI18nPackFiles = prepareI18nPackFiles; function prepareIslFiles(language, innoSetupConfig) { const parsePromises = []; return (0, event_stream_1.through)(function (xlf) { @@ -816,7 +817,6 @@ function prepareIslFiles(language, innoSetupConfig) { }); }); } -exports.prepareIslFiles = prepareIslFiles; function createIslFile(name, messages, language, innoSetup) { const content = []; let originalContent; diff --git a/code/build/lib/i18n.resources.json b/code/build/lib/i18n.resources.json index 654a4445848..b080b05f102 100644 --- a/code/build/lib/i18n.resources.json +++ b/code/build/lib/i18n.resources.json @@ -342,6 +342,10 @@ "name": "vs/workbench/contrib/bracketPairColorizer2Telemetry", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/scrollLocking", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/remoteTunnel", "project": "vscode-workbench" @@ -553,6 +557,10 @@ { "name": "vs/workbench/contrib/accountEntitlements", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/authentication", + "project": "vscode-workbench" } ] } diff --git a/code/build/lib/mangleTypeScript.js b/code/build/lib/mangleTypeScript.js deleted file mode 100644 index 45b50148d12..00000000000 --- a/code/build/lib/mangleTypeScript.js +++ /dev/null @@ -1,676 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Mangler = void 0; -const ts = require("typescript"); -const path = require("path"); -const fs = require("fs"); -const process_1 = require("process"); -const source_map_1 = require("source-map"); -const url_1 = require("url"); -class ShortIdent { - static _keywords = new Set(['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', - 'default', 'delete', 'do', 'else', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', - 'import', 'in', 'instanceof', 'let', 'new', 'null', 'return', 'static', 'super', 'switch', 'this', 'throw', - 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']); - static _alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890$_'.split(''); - _value = 0; - _isNameTaken; - prefix; - constructor(prefix, isNameTaken) { - this.prefix = prefix; - this._isNameTaken = name => ShortIdent._keywords.has(name) || /^[_0-9]/.test(name) || isNameTaken(name); - } - next(localIsNameTaken) { - const candidate = this.prefix + ShortIdent.convert(this._value); - this._value++; - if (this._isNameTaken(candidate) || localIsNameTaken?.(candidate)) { - // try again - return this.next(localIsNameTaken); - } - return candidate; - } - static convert(n) { - const base = this._alphabet.length; - let result = ''; - do { - const rest = n % base; - result += this._alphabet[rest]; - n = (n / base) | 0; - } while (n > 0); - return result; - } -} -var FieldType; -(function (FieldType) { - FieldType[FieldType["Public"] = 0] = "Public"; - FieldType[FieldType["Protected"] = 1] = "Protected"; - FieldType[FieldType["Private"] = 2] = "Private"; -})(FieldType || (FieldType = {})); -class ClassData { - fileName; - node; - fields = new Map(); - replacements; - parent; - children; - constructor(fileName, node) { - // analyse all fields (properties and methods). Find usages of all protected and - // private ones and keep track of all public ones (to prevent naming collisions) - this.fileName = fileName; - this.node = node; - const candidates = []; - for (const member of node.members) { - if (ts.isMethodDeclaration(member)) { - // method `foo() {}` - candidates.push(member); - } - else if (ts.isPropertyDeclaration(member)) { - // property `foo = 234` - candidates.push(member); - } - else if (ts.isGetAccessor(member)) { - // getter: `get foo() { ... }` - candidates.push(member); - } - else if (ts.isSetAccessor(member)) { - // setter: `set foo() { ... }` - candidates.push(member); - } - else if (ts.isConstructorDeclaration(member)) { - // constructor-prop:`constructor(private foo) {}` - for (const param of member.parameters) { - if (hasModifier(param, ts.SyntaxKind.PrivateKeyword) - || hasModifier(param, ts.SyntaxKind.ProtectedKeyword) - || hasModifier(param, ts.SyntaxKind.PublicKeyword) - || hasModifier(param, ts.SyntaxKind.ReadonlyKeyword)) { - candidates.push(param); - } - } - } - } - for (const member of candidates) { - const ident = ClassData._getMemberName(member); - if (!ident) { - continue; - } - const type = ClassData._getFieldType(member); - this.fields.set(ident, { type, pos: member.name.getStart() }); - } - } - static _getMemberName(node) { - if (!node.name) { - return undefined; - } - const { name } = node; - let ident = name.getText(); - if (name.kind === ts.SyntaxKind.ComputedPropertyName) { - if (name.expression.kind !== ts.SyntaxKind.StringLiteral) { - // unsupported: [Symbol.foo] or [abc + 'field'] - return; - } - // ['foo'] - ident = name.expression.getText().slice(1, -1); - } - return ident; - } - static _getFieldType(node) { - if (hasModifier(node, ts.SyntaxKind.PrivateKeyword)) { - return 2 /* FieldType.Private */; - } - else if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword)) { - return 1 /* FieldType.Protected */; - } - else { - return 0 /* FieldType.Public */; - } - } - static _shouldMangle(type) { - return type === 2 /* FieldType.Private */ - || type === 1 /* FieldType.Protected */; - } - static makeImplicitPublicActuallyPublic(data, reportViolation) { - // TS-HACK - // A subtype can make an inherited protected field public. To prevent accidential - // mangling of public fields we mark the original (protected) fields as public... - for (const [name, info] of data.fields) { - if (info.type !== 0 /* FieldType.Public */) { - continue; - } - let parent = data.parent; - while (parent) { - if (parent.fields.get(name)?.type === 1 /* FieldType.Protected */) { - const parentPos = parent.node.getSourceFile().getLineAndCharacterOfPosition(parent.fields.get(name).pos); - const infoPos = data.node.getSourceFile().getLineAndCharacterOfPosition(info.pos); - reportViolation(name, `'${name}' from ${parent.fileName}:${parentPos.line + 1}`, `${data.fileName}:${infoPos.line + 1}`); - parent.fields.get(name).type = 0 /* FieldType.Public */; - } - parent = parent.parent; - } - } - } - static fillInReplacement(data) { - if (data.replacements) { - // already done - return; - } - // fill in parents first - if (data.parent) { - ClassData.fillInReplacement(data.parent); - } - data.replacements = new Map(); - const identPool = new ShortIdent('', name => { - // locally taken - if (data._isNameTaken(name)) { - return true; - } - // parents - let parent = data.parent; - while (parent) { - if (parent._isNameTaken(name)) { - return true; - } - parent = parent.parent; - } - // children - if (data.children) { - const stack = [...data.children]; - while (stack.length) { - const node = stack.pop(); - if (node._isNameTaken(name)) { - return true; - } - if (node.children) { - stack.push(...node.children); - } - } - } - return false; - }); - for (const [name, info] of data.fields) { - if (ClassData._shouldMangle(info.type)) { - const shortName = identPool.next(); - data.replacements.set(name, shortName); - } - } - } - // a name is taken when a field that doesn't get mangled exists or - // when the name is already in use for replacement - _isNameTaken(name) { - if (this.fields.has(name) && !ClassData._shouldMangle(this.fields.get(name).type)) { - // public field - return true; - } - if (this.replacements) { - for (const shortName of this.replacements.values()) { - if (shortName === name) { - // replaced already (happens wih super types) - return true; - } - } - } - if (isNameTakenInFile(this.node, name)) { - return true; - } - return false; - } - lookupShortName(name) { - let value = this.replacements.get(name); - let parent = this.parent; - while (parent) { - if (parent.replacements.has(name) && parent.fields.get(name)?.type === 1 /* FieldType.Protected */) { - value = parent.replacements.get(name) ?? value; - } - parent = parent.parent; - } - return value; - } - // --- parent chaining - addChild(child) { - this.children ??= []; - this.children.push(child); - child.parent = this; - } -} -function isNameTakenInFile(node, name) { - const identifiers = node.getSourceFile().identifiers; - if (identifiers instanceof Map) { - if (identifiers.has(name)) { - return true; - } - } - return false; -} -const fileIdents = new class { - idents = new ShortIdent('$', () => false); - next(file) { - return this.idents.next(name => isNameTakenInFile(file, name)); - } -}; -const skippedFiles = [ - // Build - 'css.build.ts', - 'nls.build.ts', - // Monaco - 'editorCommon.ts', - 'editorOptions.ts', - 'editorZoom.ts', - 'standaloneEditor.ts', - 'standaloneLanguages.ts', - // Generated - 'extensionsApiProposals.ts', - // Module passed around as type - 'pfs.ts', -]; -class DeclarationData { - fileName; - node; - replacementName; - constructor(fileName, node) { - this.fileName = fileName; - this.node = node; - this.replacementName = fileIdents.next(node.getSourceFile()); - } - get locations() { - return [{ - fileName: this.fileName, - offset: this.node.name.getStart() - }]; - } - shouldMangle(newName) { - // New name is longer the existing one :'( - if (newName.length >= this.node.name.getText().length) { - return false; - } - // Don't mangle functions we've explicitly opted out - if (this.node.getFullText().includes('@skipMangle')) { - return false; - } - // Don't mangle functions in the monaco editor API. - if (skippedFiles.some(file => this.node.getSourceFile().fileName.endsWith(file))) { - return false; - } - return true; - } -} -class ConstData { - fileName; - statement; - decl; - service; - replacementName; - constructor(fileName, statement, decl, service) { - this.fileName = fileName; - this.statement = statement; - this.decl = decl; - this.service = service; - this.replacementName = fileIdents.next(statement.getSourceFile()); - } - get locations() { - // If the const aliases any types, we need to rename those too - const definitionResult = this.service.getDefinitionAndBoundSpan(this.decl.getSourceFile().fileName, this.decl.name.getStart()); - if (definitionResult?.definitions && definitionResult.definitions.length > 1) { - return definitionResult.definitions.map(x => ({ fileName: x.fileName, offset: x.textSpan.start })); - } - return [{ fileName: this.fileName, offset: this.decl.name.getStart() }]; - } - shouldMangle(newName) { - // New name is longer the existing one :'( - if (newName.length >= this.decl.name.getText().length) { - return false; - } - // Don't mangle functions we've explicitly opted out - if (this.statement.getFullText().includes('@skipMangle')) { - return false; - } - // Don't mangle functions in some files - if (skippedFiles.some(file => this.decl.getSourceFile().fileName.endsWith(file))) { - return false; - } - return true; - } -} -class StaticLanguageServiceHost { - projectPath; - _cmdLine; - _scriptSnapshots = new Map(); - constructor(projectPath) { - this.projectPath = projectPath; - const existingOptions = {}; - const parsed = ts.readConfigFile(projectPath, ts.sys.readFile); - if (parsed.error) { - throw parsed.error; - } - this._cmdLine = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(projectPath), existingOptions); - if (this._cmdLine.errors.length > 0) { - throw parsed.error; - } - } - getCompilationSettings() { - return this._cmdLine.options; - } - getScriptFileNames() { - return this._cmdLine.fileNames; - } - getScriptVersion(_fileName) { - return '1'; - } - getProjectVersion() { - return '1'; - } - getScriptSnapshot(fileName) { - let result = this._scriptSnapshots.get(fileName); - if (result === undefined) { - const content = ts.sys.readFile(fileName); - if (content === undefined) { - return undefined; - } - result = ts.ScriptSnapshot.fromString(content); - this._scriptSnapshots.set(fileName, result); - } - return result; - } - getCurrentDirectory() { - return path.dirname(this.projectPath); - } - getDefaultLibFileName(options) { - return ts.getDefaultLibFilePath(options); - } - directoryExists = ts.sys.directoryExists; - getDirectories = ts.sys.getDirectories; - fileExists = ts.sys.fileExists; - readFile = ts.sys.readFile; - readDirectory = ts.sys.readDirectory; - // this is necessary to make source references work. - realpath = ts.sys.realpath; -} -/** - * TypeScript2TypeScript transformer that mangles all private and protected fields - * - * 1. Collect all class fields (properties, methods) - * 2. Collect all sub and super-type relations between classes - * 3. Compute replacement names for each field - * 4. Lookup rename locations for these fields - * 5. Prepare and apply edits - */ -class Mangler { - projectPath; - log; - allClassDataByKey = new Map(); - allExportedDeclarationsByKey = new Map(); - service; - constructor(projectPath, log = () => { }) { - this.projectPath = projectPath; - this.log = log; - this.service = ts.createLanguageService(new StaticLanguageServiceHost(projectPath)); - } - computeNewFileContents(strictImplicitPublicHandling) { - // STEP find all classes and their field info. Find all exported consts and functions. - const visit = (node) => { - if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { - const anchor = node.name ?? node; - const key = `${node.getSourceFile().fileName}|${anchor.getStart()}`; - if (this.allClassDataByKey.has(key)) { - throw new Error('DUPE?'); - } - this.allClassDataByKey.set(key, new ClassData(node.getSourceFile().fileName, node)); - } - if (ts.isClassDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) { - if (node.name) { - const anchor = node.name; - const key = `${node.getSourceFile().fileName}|${anchor.getStart()}`; - if (this.allExportedDeclarationsByKey.has(key)) { - throw new Error('DUPE?'); - } - this.allExportedDeclarationsByKey.set(key, new DeclarationData(node.getSourceFile().fileName, node)); - } - } - if (ts.isFunctionDeclaration(node) - && ts.isSourceFile(node.parent) - && hasModifier(node, ts.SyntaxKind.ExportKeyword)) { - if (node.name && node.body) { // On named function and not on the overload - const anchor = node.name; - const key = `${node.getSourceFile().fileName}|${anchor.getStart()}`; - if (this.allExportedDeclarationsByKey.has(key)) { - throw new Error('DUPE?'); - } - this.allExportedDeclarationsByKey.set(key, new DeclarationData(node.getSourceFile().fileName, node)); - } - } - if (ts.isVariableStatement(node) - && ts.isSourceFile(node.parent) - && hasModifier(node, ts.SyntaxKind.ExportKeyword)) { - for (const decl of node.declarationList.declarations) { - const key = `${decl.getSourceFile().fileName}|${decl.name.getStart()}`; - if (this.allExportedDeclarationsByKey.has(key)) { - throw new Error('DUPE?'); - } - this.allExportedDeclarationsByKey.set(key, new ConstData(node.getSourceFile().fileName, node, decl, this.service)); - } - } - ts.forEachChild(node, visit); - }; - for (const file of this.service.getProgram().getSourceFiles()) { - if (!file.isDeclarationFile) { - ts.forEachChild(file, visit); - } - } - this.log(`Done collecting. Classes: ${this.allClassDataByKey.size}. Exported const/fn: ${this.allExportedDeclarationsByKey.size}`); - // STEP: connect sub and super-types - const setupParents = (data) => { - const extendsClause = data.node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword); - if (!extendsClause) { - // no EXTENDS-clause - return; - } - const info = this.service.getDefinitionAtPosition(data.fileName, extendsClause.types[0].expression.getEnd()); - if (!info || info.length === 0) { - // throw new Error('SUPER type not found'); - return; - } - if (info.length !== 1) { - // inherits from declared/library type - return; - } - const [definition] = info; - const key = `${definition.fileName}|${definition.textSpan.start}`; - const parent = this.allClassDataByKey.get(key); - if (!parent) { - // throw new Error(`SUPER type not found: ${key}`); - return; - } - parent.addChild(data); - }; - for (const data of this.allClassDataByKey.values()) { - setupParents(data); - } - // STEP: make implicit public (actually protected) field really public - const violations = new Map(); - let violationsCauseFailure = false; - for (const data of this.allClassDataByKey.values()) { - ClassData.makeImplicitPublicActuallyPublic(data, (name, what, why) => { - const arr = violations.get(what); - if (arr) { - arr.push(why); - } - else { - violations.set(what, [why]); - } - if (strictImplicitPublicHandling && !strictImplicitPublicHandling.has(name)) { - violationsCauseFailure = true; - } - }); - } - for (const [why, whys] of violations) { - this.log(`WARN: ${why} became PUBLIC because of: ${whys.join(' , ')}`); - } - if (violationsCauseFailure) { - const message = 'Protected fields have been made PUBLIC. This hurts minification and is therefore not allowed. Review the WARN messages further above'; - this.log(`ERROR: ${message}`); - throw new Error(message); - } - // STEP: compute replacement names for each class - for (const data of this.allClassDataByKey.values()) { - ClassData.fillInReplacement(data); - } - this.log(`Done creating class replacements`); - const editsByFile = new Map(); - const appendEdit = (fileName, edit) => { - const edits = editsByFile.get(fileName); - if (!edits) { - editsByFile.set(fileName, [edit]); - } - else { - edits.push(edit); - } - }; - const appendRename = (newText, loc) => { - appendEdit(loc.fileName, { - newText: (loc.prefixText || '') + newText + (loc.suffixText || ''), - offset: loc.textSpan.start, - length: loc.textSpan.length - }); - }; - for (const data of this.allClassDataByKey.values()) { - if (hasModifier(data.node, ts.SyntaxKind.DeclareKeyword)) { - continue; - } - fields: for (const [name, info] of data.fields) { - if (!ClassData._shouldMangle(info.type)) { - continue fields; - } - // TS-HACK: protected became public via 'some' child - // and because of that we might need to ignore this now - let parent = data.parent; - while (parent) { - if (parent.fields.get(name)?.type === 0 /* FieldType.Public */) { - continue fields; - } - parent = parent.parent; - } - const newText = data.lookupShortName(name); - const locations = this.service.findRenameLocations(data.fileName, info.pos, false, false, true) ?? []; - for (const loc of locations) { - appendRename(newText, loc); - } - } - } - for (const data of this.allExportedDeclarationsByKey.values()) { - if (!data.shouldMangle(data.replacementName)) { - continue; - } - const newText = data.replacementName; - for (const { fileName, offset } of data.locations) { - const locations = this.service.findRenameLocations(fileName, offset, false, false, true) ?? []; - for (const loc of locations) { - appendRename(newText, loc); - } - } - } - this.log(`Done preparing edits: ${editsByFile.size} files`); - // STEP: apply all rename edits (per file) - const result = new Map(); - let savedBytes = 0; - for (const item of this.service.getProgram().getSourceFiles()) { - const { mapRoot, sourceRoot } = this.service.getProgram().getCompilerOptions(); - const projectDir = path.dirname(this.projectPath); - const sourceMapRoot = mapRoot ?? (0, url_1.pathToFileURL)(sourceRoot ?? projectDir).toString(); - // source maps - let generator; - let newFullText; - const edits = editsByFile.get(item.fileName); - if (!edits) { - // just copy - newFullText = item.getFullText(); - } - else { - // source map generator - const relativeFileName = normalize(path.relative(projectDir, item.fileName)); - const mappingsByLine = new Map(); - // apply renames - edits.sort((a, b) => b.offset - a.offset); - const characters = item.getFullText().split(''); - let lastEdit; - for (const edit of edits) { - if (lastEdit && lastEdit.offset === edit.offset) { - // - if (lastEdit.length !== edit.length || lastEdit.newText !== edit.newText) { - this.log('ERROR: Overlapping edit', item.fileName, edit.offset, edits); - throw new Error('OVERLAPPING edit'); - } - else { - continue; - } - } - lastEdit = edit; - const mangledName = characters.splice(edit.offset, edit.length, edit.newText).join(''); - savedBytes += mangledName.length - edit.newText.length; - // source maps - const pos = item.getLineAndCharacterOfPosition(edit.offset); - let mappings = mappingsByLine.get(pos.line); - if (!mappings) { - mappings = []; - mappingsByLine.set(pos.line, mappings); - } - mappings.unshift({ - source: relativeFileName, - original: { line: pos.line + 1, column: pos.character }, - generated: { line: pos.line + 1, column: pos.character }, - name: mangledName - }, { - source: relativeFileName, - original: { line: pos.line + 1, column: pos.character + edit.length }, - generated: { line: pos.line + 1, column: pos.character + edit.newText.length }, - }); - } - // source map generation, make sure to get mappings per line correct - generator = new source_map_1.SourceMapGenerator({ file: path.basename(item.fileName), sourceRoot: sourceMapRoot }); - generator.setSourceContent(relativeFileName, item.getFullText()); - for (const [, mappings] of mappingsByLine) { - let lineDelta = 0; - for (const mapping of mappings) { - generator.addMapping({ - ...mapping, - generated: { line: mapping.generated.line, column: mapping.generated.column - lineDelta } - }); - lineDelta += mapping.original.column - mapping.generated.column; - } - } - newFullText = characters.join(''); - } - result.set(item.fileName, { out: newFullText, sourceMap: generator?.toString() }); - } - this.log(`Done: ${savedBytes / 1000}kb saved`); - return result; - } -} -exports.Mangler = Mangler; -// --- ast utils -function hasModifier(node, kind) { - const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined; - return Boolean(modifiers?.find(mode => mode.kind === kind)); -} -function normalize(path) { - return path.replace(/\\/g, '/'); -} -async function _run() { - const projectPath = path.join(__dirname, '../../src/tsconfig.json'); - const projectBase = path.dirname(projectPath); - const newProjectBase = path.join(path.dirname(projectBase), path.basename(projectBase) + '2'); - fs.cpSync(projectBase, newProjectBase, { recursive: true }); - for await (const [fileName, contents] of new Mangler(projectPath, console.log).computeNewFileContents(new Set(['saveState']))) { - const newFilePath = path.join(newProjectBase, path.relative(projectBase, fileName)); - await fs.promises.mkdir(path.dirname(newFilePath), { recursive: true }); - await fs.promises.writeFile(newFilePath, contents.out); - if (contents.sourceMap) { - await fs.promises.writeFile(newFilePath + '.map', contents.sourceMap); - } - } -} -if (__filename === process_1.argv[1]) { - _run(); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuZ2xlVHlwZVNjcmlwdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIm1hbmdsZVR5cGVTY3JpcHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOzs7QUFFaEcsaUNBQWlDO0FBQ2pDLDZCQUE2QjtBQUM3Qix5QkFBeUI7QUFDekIscUNBQStCO0FBQy9CLDJDQUF5RDtBQUN6RCw2QkFBb0M7QUFFcEMsTUFBTSxVQUFVO0lBRVAsTUFBTSxDQUFDLFNBQVMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxVQUFVO1FBQzlHLFNBQVMsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxTQUFTLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxJQUFJO1FBQ25HLFFBQVEsRUFBRSxJQUFJLEVBQUUsWUFBWSxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUUsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsT0FBTztRQUMxRyxNQUFNLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQztJQUU1RCxNQUFNLENBQUMsU0FBUyxHQUFHLGtFQUFrRSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUVoRyxNQUFNLEdBQUcsQ0FBQyxDQUFDO0lBQ0YsWUFBWSxDQUE0QjtJQUN4QyxNQUFNLENBQVM7SUFFaEMsWUFBWSxNQUFjLEVBQUUsV0FBc0M7UUFDakUsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7UUFDckIsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFJLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksV0FBVyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3pHLENBQUM7SUFFRCxJQUFJLENBQUMsZ0JBQTRDO1FBQ2hELE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQyxNQUFNLEdBQUcsVUFBVSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDaEUsSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ2QsSUFBSSxJQUFJLENBQUMsWUFBWSxDQUFDLFNBQVMsQ0FBQyxJQUFJLGdCQUFnQixFQUFFLENBQUMsU0FBUyxDQUFDLEVBQUU7WUFDbEUsWUFBWTtZQUNaLE9BQU8sSUFBSSxDQUFDLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO1NBQ25DO1FBQ0QsT0FBTyxTQUFTLENBQUM7SUFDbEIsQ0FBQztJQUVPLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBUztRQUMvQixNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQztRQUNuQyxJQUFJLE1BQU0sR0FBRyxFQUFFLENBQUM7UUFDaEIsR0FBRztZQUNGLE1BQU0sSUFBSSxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUM7WUFDdEIsTUFBTSxJQUFJLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDL0IsQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztTQUNuQixRQUFRLENBQUMsR0FBRyxDQUFDLEVBQUU7UUFDaEIsT0FBTyxNQUFNLENBQUM7SUFDZixDQUFDOztBQUdGLElBQVcsU0FJVjtBQUpELFdBQVcsU0FBUztJQUNuQiw2Q0FBTSxDQUFBO0lBQ04sbURBQVMsQ0FBQTtJQUNULCtDQUFPLENBQUE7QUFDUixDQUFDLEVBSlUsU0FBUyxLQUFULFNBQVMsUUFJbkI7QUFFRCxNQUFNLFNBQVM7SUFVSjtJQUNBO0lBVFYsTUFBTSxHQUFHLElBQUksR0FBRyxFQUE0QyxDQUFDO0lBRXJELFlBQVksQ0FBa0M7SUFFdEQsTUFBTSxDQUF3QjtJQUM5QixRQUFRLENBQTBCO0lBRWxDLFlBQ1UsUUFBZ0IsRUFDaEIsSUFBOEM7UUFFdkQsZ0ZBQWdGO1FBQ2hGLGdGQUFnRjtRQUp2RSxhQUFRLEdBQVIsUUFBUSxDQUFRO1FBQ2hCLFNBQUksR0FBSixJQUFJLENBQTBDO1FBS3ZELE1BQU0sVUFBVSxHQUE0QixFQUFFLENBQUM7UUFDL0MsS0FBSyxNQUFNLE1BQU0sSUFBSSxJQUFJLENBQUMsT0FBTyxFQUFFO1lBQ2xDLElBQUksRUFBRSxDQUFDLG1CQUFtQixDQUFDLE1BQU0sQ0FBQyxFQUFFO2dCQUNuQyxvQkFBb0I7Z0JBQ3BCLFVBQVUsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7YUFFeEI7aUJBQU0sSUFBSSxFQUFFLENBQUMscUJBQXFCLENBQUMsTUFBTSxDQUFDLEVBQUU7Z0JBQzVDLHVCQUF1QjtnQkFDdkIsVUFBVSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQzthQUV4QjtpQkFBTSxJQUFJLEVBQUUsQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFDLEVBQUU7Z0JBQ3BDLDhCQUE4QjtnQkFDOUIsVUFBVSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQzthQUV4QjtpQkFBTSxJQUFJLEVBQUUsQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFDLEVBQUU7Z0JBQ3BDLDhCQUE4QjtnQkFDOUIsVUFBVSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQzthQUV4QjtpQkFBTSxJQUFJLEVBQUUsQ0FBQyx3QkFBd0IsQ0FBQyxNQUFNLENBQUMsRUFBRTtnQkFDL0MsaURBQWlEO2dCQUNqRCxLQUFLLE1BQU0sS0FBSyxJQUFJLE1BQU0sQ0FBQyxVQUFVLEVBQUU7b0JBQ3RDLElBQUksV0FBVyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLGNBQWMsQ0FBQzsyQkFDaEQsV0FBVyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLGdCQUFnQixDQUFDOzJCQUNsRCxXQUFXLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsYUFBYSxDQUFDOzJCQUMvQyxXQUFXLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsZUFBZSxDQUFDLEVBQ25EO3dCQUNELFVBQVUsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7cUJBQ3ZCO2lCQUNEO2FBQ0Q7U0FDRDtRQUNELEtBQUssTUFBTSxNQUFNLElBQUksVUFBVSxFQUFFO1lBQ2hDLE1BQU0sS0FBSyxHQUFHLFNBQVMsQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLENBQUM7WUFDL0MsSUFBSSxDQUFDLEtBQUssRUFBRTtnQkFDWCxTQUFTO2FBQ1Q7WUFDRCxNQUFNLElBQUksR0FBRyxTQUFTLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQzdDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsTUFBTSxDQUFDLElBQUssQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7U0FDL0Q7SUFDRixDQUFDO0lBRU8sTUFBTSxDQUFDLGNBQWMsQ0FBQyxJQUF5QjtRQUN0RCxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRTtZQUNmLE9BQU8sU0FBUyxDQUFDO1NBQ2pCO1FBQ0QsTUFBTSxFQUFFLElBQUksRUFBRSxHQUFHLElBQUksQ0FBQztRQUN0QixJQUFJLEtBQUssR0FBRyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDM0IsSUFBSSxJQUFJLENBQUMsSUFBSSxLQUFLLEVBQUUsQ0FBQyxVQUFVLENBQUMsb0JBQW9CLEVBQUU7WUFDckQsSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksS0FBSyxFQUFFLENBQUMsVUFBVSxDQUFDLGFBQWEsRUFBRTtnQkFDekQsK0NBQStDO2dCQUMvQyxPQUFPO2FBQ1A7WUFDRCxVQUFVO1lBQ1YsS0FBSyxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQy9DO1FBRUQsT0FBTyxLQUFLLENBQUM7SUFDZCxDQUFDO0lBRU8sTUFBTSxDQUFDLGFBQWEsQ0FBQyxJQUFhO1FBQ3pDLElBQUksV0FBVyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLGNBQWMsQ0FBQyxFQUFFO1lBQ3BELGlDQUF5QjtTQUN6QjthQUFNLElBQUksV0FBVyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLGdCQUFnQixDQUFDLEVBQUU7WUFDN0QsbUNBQTJCO1NBQzNCO2FBQU07WUFDTixnQ0FBd0I7U0FDeEI7SUFDRixDQUFDO0lBRUQsTUFBTSxDQUFDLGFBQWEsQ0FBQyxJQUFlO1FBQ25DLE9BQU8sSUFBSSw4QkFBc0I7ZUFDN0IsSUFBSSxnQ0FBd0IsQ0FDOUI7SUFDSCxDQUFDO0lBRUQsTUFBTSxDQUFDLGdDQUFnQyxDQUFDLElBQWUsRUFBRSxlQUFrRTtRQUMxSCxVQUFVO1FBQ1YsaUZBQWlGO1FBQ2pGLGlGQUFpRjtRQUNqRixLQUFLLE1BQU0sQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUksSUFBSSxDQUFDLE1BQU0sRUFBRTtZQUN2QyxJQUFJLElBQUksQ0FBQyxJQUFJLDZCQUFxQixFQUFFO2dCQUNuQyxTQUFTO2FBQ1Q7WUFDRCxJQUFJLE1BQU0sR0FBMEIsSUFBSSxDQUFDLE1BQU0sQ0FBQztZQUNoRCxPQUFPLE1BQU0sRUFBRTtnQkFDZCxJQUFJLE1BQU0sQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLElBQUksZ0NBQXdCLEVBQUU7b0JBQzFELE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsNkJBQTZCLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFFLENBQUMsR0FBRyxDQUFDLENBQUM7b0JBQzFHLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsNkJBQTZCLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO29CQUNsRixlQUFlLENBQUMsSUFBSSxFQUFFLElBQUksSUFBSSxVQUFVLE1BQU0sQ0FBQyxRQUFRLElBQUksU0FBUyxDQUFDLElBQUksR0FBRyxDQUFDLEVBQUUsRUFBRSxHQUFHLElBQUksQ0FBQyxRQUFRLElBQUksT0FBTyxDQUFDLElBQUksR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFDO29CQUV6SCxNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUUsQ0FBQyxJQUFJLDJCQUFtQixDQUFDO2lCQUNqRDtnQkFDRCxNQUFNLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQzthQUN2QjtTQUNEO0lBQ0YsQ0FBQztJQUVELE1BQU0sQ0FBQyxpQkFBaUIsQ0FBQyxJQUFlO1FBRXZDLElBQUksSUFBSSxDQUFDLFlBQVksRUFBRTtZQUN0QixlQUFlO1lBQ2YsT0FBTztTQUNQO1FBRUQsd0JBQXdCO1FBQ3hCLElBQUksSUFBSSxDQUFDLE1BQU0sRUFBRTtZQUNoQixTQUFTLENBQUMsaUJBQWlCLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1NBQ3pDO1FBRUQsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBRTlCLE1BQU0sU0FBUyxHQUFHLElBQUksVUFBVSxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsRUFBRTtZQUUzQyxnQkFBZ0I7WUFDaEIsSUFBSSxJQUFJLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxFQUFFO2dCQUM1QixPQUFPLElBQUksQ0FBQzthQUNaO1lBRUQsVUFBVTtZQUNWLElBQUksTUFBTSxHQUEwQixJQUFJLENBQUMsTUFBTSxDQUFDO1lBQ2hELE9BQU8sTUFBTSxFQUFFO2dCQUNkLElBQUksTUFBTSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsRUFBRTtvQkFDOUIsT0FBTyxJQUFJLENBQUM7aUJBQ1o7Z0JBQ0QsTUFBTSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7YUFDdkI7WUFFRCxXQUFXO1lBQ1gsSUFBSSxJQUFJLENBQUMsUUFBUSxFQUFFO2dCQUNsQixNQUFNLEtBQUssR0FBRyxDQUFDLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO2dCQUNqQyxPQUFPLEtBQUssQ0FBQyxNQUFNLEVBQUU7b0JBQ3BCLE1BQU0sSUFBSSxHQUFHLEtBQUssQ0FBQyxHQUFHLEVBQUcsQ0FBQztvQkFDMUIsSUFBSSxJQUFJLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxFQUFFO3dCQUM1QixPQUFPLElBQUksQ0FBQztxQkFDWjtvQkFDRCxJQUFJLElBQUksQ0FBQyxRQUFRLEVBQUU7d0JBQ2xCLEtBQUssQ0FBQyxJQUFJLENBQUMsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUM7cUJBQzdCO2lCQUNEO2FBQ0Q7WUFFRCxPQUFPLEtBQUssQ0FBQztRQUNkLENBQUMsQ0FBQyxDQUFDO1FBRUgsS0FBSyxNQUFNLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxNQUFNLEVBQUU7WUFDdkMsSUFBSSxTQUFTLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRTtnQkFDdkMsTUFBTSxTQUFTLEdBQUcsU0FBUyxDQUFDLElBQUksRUFBRSxDQUFDO2dCQUNuQyxJQUFJLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7YUFDdkM7U0FDRDtJQUNGLENBQUM7SUFFRCxrRUFBa0U7SUFDbEUsa0RBQWtEO0lBQzFDLFlBQVksQ0FBQyxJQUFZO1FBQ2hDLElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBRSxDQUFDLElBQUksQ0FBQyxFQUFFO1lBQ25GLGVBQWU7WUFDZixPQUFPLElBQUksQ0FBQztTQUNaO1FBQ0QsSUFBSSxJQUFJLENBQUMsWUFBWSxFQUFFO1lBQ3RCLEtBQUssTUFBTSxTQUFTLElBQUksSUFBSSxDQUFDLFlBQVksQ0FBQyxNQUFNLEVBQUUsRUFBRTtnQkFDbkQsSUFBSSxTQUFTLEtBQUssSUFBSSxFQUFFO29CQUN2Qiw2Q0FBNkM7b0JBQzdDLE9BQU8sSUFBSSxDQUFDO2lCQUNaO2FBQ0Q7U0FDRDtRQUVELElBQUksaUJBQWlCLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxJQUFJLENBQUMsRUFBRTtZQUN2QyxPQUFPLElBQUksQ0FBQztTQUNaO1FBRUQsT0FBTyxLQUFLLENBQUM7SUFDZCxDQUFDO0lBRUQsZUFBZSxDQUFDLElBQVk7UUFDM0IsSUFBSSxLQUFLLEdBQUcsSUFBSSxDQUFDLFlBQWEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFFLENBQUM7UUFDMUMsSUFBSSxNQUFNLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQztRQUN6QixPQUFPLE1BQU0sRUFBRTtZQUNkLElBQUksTUFBTSxDQUFDLFlBQWEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksTUFBTSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsSUFBSSxnQ0FBd0IsRUFBRTtnQkFDNUYsS0FBSyxHQUFHLE1BQU0sQ0FBQyxZQUFhLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBRSxJQUFJLEtBQUssQ0FBQzthQUNqRDtZQUNELE1BQU0sR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO1NBQ3ZCO1FBQ0QsT0FBTyxLQUFLLENBQUM7SUFDZCxDQUFDO0lBRUQsc0JBQXNCO0lBRXRCLFFBQVEsQ0FBQyxLQUFnQjtRQUN4QixJQUFJLENBQUMsUUFBUSxLQUFLLEVBQUUsQ0FBQztRQUNyQixJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUMxQixLQUFLLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQztJQUNyQixDQUFDO0NBQ0Q7QUFFRCxTQUFTLGlCQUFpQixDQUFDLElBQWEsRUFBRSxJQUFZO0lBQ3JELE1BQU0sV0FBVyxHQUFTLElBQUksQ0FBQyxhQUFhLEVBQUcsQ0FBQyxXQUFXLENBQUM7SUFDNUQsSUFBSSxXQUFXLFlBQVksR0FBRyxFQUFFO1FBQy9CLElBQUksV0FBVyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRTtZQUMxQixPQUFPLElBQUksQ0FBQztTQUNaO0tBQ0Q7SUFDRCxPQUFPLEtBQUssQ0FBQztBQUNkLENBQUM7QUFFRCxNQUFNLFVBQVUsR0FBRyxJQUFJO0lBQ0wsTUFBTSxHQUFHLElBQUksVUFBVSxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUUzRCxJQUFJLENBQUMsSUFBbUI7UUFDdkIsT0FBTyxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLGlCQUFpQixDQUFDLElBQUksRUFBRSxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ2hFLENBQUM7Q0FDRCxDQUFDO0FBRUYsTUFBTSxZQUFZLEdBQUc7SUFDcEIsUUFBUTtJQUNSLGNBQWM7SUFDZCxjQUFjO0lBRWQsU0FBUztJQUNULGlCQUFpQjtJQUNqQixrQkFBa0I7SUFDbEIsZUFBZTtJQUNmLHFCQUFxQjtJQUNyQix3QkFBd0I7SUFFeEIsWUFBWTtJQUNaLDJCQUEyQjtJQUUzQiwrQkFBK0I7SUFDL0IsUUFBUTtDQUNSLENBQUM7QUFFRixNQUFNLGVBQWU7SUFLVjtJQUNBO0lBSkQsZUFBZSxDQUFTO0lBRWpDLFlBQ1UsUUFBZ0IsRUFDaEIsSUFBa0Q7UUFEbEQsYUFBUSxHQUFSLFFBQVEsQ0FBUTtRQUNoQixTQUFJLEdBQUosSUFBSSxDQUE4QztRQUUzRCxJQUFJLENBQUMsZUFBZSxHQUFHLFVBQVUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDLENBQUM7SUFDOUQsQ0FBQztJQUVELElBQUksU0FBUztRQUNaLE9BQU8sQ0FBQztnQkFDUCxRQUFRLEVBQUUsSUFBSSxDQUFDLFFBQVE7Z0JBQ3ZCLE1BQU0sRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUssQ0FBQyxRQUFRLEVBQUU7YUFDbEMsQ0FBQyxDQUFDO0lBQ0osQ0FBQztJQUVELFlBQVksQ0FBQyxPQUFlO1FBQzNCLDBDQUEwQztRQUMxQyxJQUFJLE9BQU8sQ0FBQyxNQUFNLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsTUFBTSxFQUFFO1lBQ3ZELE9BQU8sS0FBSyxDQUFDO1NBQ2I7UUFFRCxvREFBb0Q7UUFDcEQsSUFBSSxJQUFJLENBQUMsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxhQUFhLENBQUMsRUFBRTtZQUNwRCxPQUFPLEtBQUssQ0FBQztTQUNiO1FBRUQsbURBQW1EO1FBQ25ELElBQUksWUFBWSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxFQUFFO1lBQ2pGLE9BQU8sS0FBSyxDQUFDO1NBQ2I7UUFFRCxPQUFPLElBQUksQ0FBQztJQUNiLENBQUM7Q0FDRDtBQUVELE1BQU0sU0FBUztJQUtKO0lBQ0E7SUFDQTtJQUNRO0lBTlQsZUFBZSxDQUFTO0lBRWpDLFlBQ1UsUUFBZ0IsRUFDaEIsU0FBK0IsRUFDL0IsSUFBNEIsRUFDcEIsT0FBMkI7UUFIbkMsYUFBUSxHQUFSLFFBQVEsQ0FBUTtRQUNoQixjQUFTLEdBQVQsU0FBUyxDQUFzQjtRQUMvQixTQUFJLEdBQUosSUFBSSxDQUF3QjtRQUNwQixZQUFPLEdBQVAsT0FBTyxDQUFvQjtRQUU1QyxJQUFJLENBQUMsZUFBZSxHQUFHLFVBQVUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLGFBQWEsRUFBRSxDQUFDLENBQUM7SUFDbkUsQ0FBQztJQUVELElBQUksU0FBUztRQUNaLDhEQUE4RDtRQUM5RCxNQUFNLGdCQUFnQixHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMseUJBQXlCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQyxRQUFRLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztRQUMvSCxJQUFJLGdCQUFnQixFQUFFLFdBQVcsSUFBSSxnQkFBZ0IsQ0FBQyxXQUFXLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRTtZQUM3RSxPQUFPLGdCQUFnQixDQUFDLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQyxRQUFRLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDO1NBQ25HO1FBRUQsT0FBTyxDQUFDLEVBQUUsUUFBUSxFQUFFLElBQUksQ0FBQyxRQUFRLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUMsQ0FBQztJQUN6RSxDQUFDO0lBRUQsWUFBWSxDQUFDLE9BQWU7UUFDM0IsMENBQTBDO1FBQzFDLElBQUksT0FBTyxDQUFDLE1BQU0sSUFBSSxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQyxNQUFNLEVBQUU7WUFDdEQsT0FBTyxLQUFLLENBQUM7U0FDYjtRQUVELG9EQUFvRDtRQUNwRCxJQUFJLElBQUksQ0FBQyxTQUFTLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLGFBQWEsQ0FBQyxFQUFFO1lBQ3pELE9BQU8sS0FBSyxDQUFDO1NBQ2I7UUFFRCx1Q0FBdUM7UUFDdkMsSUFBSSxZQUFZLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLEVBQUU7WUFDakYsT0FBTyxLQUFLLENBQUM7U0FDYjtRQUVELE9BQU8sSUFBSSxDQUFDO0lBQ2IsQ0FBQztDQUNEO0FBRUQsTUFBTSx5QkFBeUI7SUFLVDtJQUhKLFFBQVEsQ0FBdUI7SUFDL0IsZ0JBQWdCLEdBQW9DLElBQUksR0FBRyxFQUFFLENBQUM7SUFFL0UsWUFBcUIsV0FBbUI7UUFBbkIsZ0JBQVcsR0FBWCxXQUFXLENBQVE7UUFDdkMsTUFBTSxlQUFlLEdBQWdDLEVBQUUsQ0FBQztRQUN4RCxNQUFNLE1BQU0sR0FBRyxFQUFFLENBQUMsY0FBYyxDQUFDLFdBQVcsRUFBRSxFQUFFLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQy9ELElBQUksTUFBTSxDQUFDLEtBQUssRUFBRTtZQUNqQixNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUM7U0FDbkI7UUFDRCxJQUFJLENBQUMsUUFBUSxHQUFHLEVBQUUsQ0FBQywwQkFBMEIsQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxHQUFHLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsRUFBRSxlQUFlLENBQUMsQ0FBQztRQUNqSCxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUU7WUFDcEMsTUFBTSxNQUFNLENBQUMsS0FBSyxDQUFDO1NBQ25CO0lBQ0YsQ0FBQztJQUNELHNCQUFzQjtRQUNyQixPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDO0lBQzlCLENBQUM7SUFDRCxrQkFBa0I7UUFDakIsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQztJQUNoQyxDQUFDO0lBQ0QsZ0JBQWdCLENBQUMsU0FBaUI7UUFDakMsT0FBTyxHQUFHLENBQUM7SUFDWixDQUFDO0lBQ0QsaUJBQWlCO1FBQ2hCLE9BQU8sR0FBRyxDQUFDO0lBQ1osQ0FBQztJQUNELGlCQUFpQixDQUFDLFFBQWdCO1FBQ2pDLElBQUksTUFBTSxHQUFtQyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ2pGLElBQUksTUFBTSxLQUFLLFNBQVMsRUFBRTtZQUN6QixNQUFNLE9BQU8sR0FBRyxFQUFFLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUMxQyxJQUFJLE9BQU8sS0FBSyxTQUFTLEVBQUU7Z0JBQzFCLE9BQU8sU0FBUyxDQUFDO2FBQ2pCO1lBQ0QsTUFBTSxHQUFHLEVBQUUsQ0FBQyxjQUFjLENBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBQy9DLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1NBQzVDO1FBQ0QsT0FBTyxNQUFNLENBQUM7SUFDZixDQUFDO0lBQ0QsbUJBQW1CO1FBQ2xCLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLENBQUM7SUFDdkMsQ0FBQztJQUNELHFCQUFxQixDQUFDLE9BQTJCO1FBQ2hELE9BQU8sRUFBRSxDQUFDLHFCQUFxQixDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQzFDLENBQUM7SUFDRCxlQUFlLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxlQUFlLENBQUM7SUFDekMsY0FBYyxHQUFHLEVBQUUsQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDO0lBQ3ZDLFVBQVUsR0FBRyxFQUFFLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQztJQUMvQixRQUFRLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUM7SUFDM0IsYUFBYSxHQUFHLEVBQUUsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDO0lBQ3JDLG9EQUFvRDtJQUNwRCxRQUFRLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUM7Q0FDM0I7QUFPRDs7Ozs7Ozs7R0FRRztBQUNILE1BQWEsT0FBTztJQU9FO0lBQThCO0lBTGxDLGlCQUFpQixHQUFHLElBQUksR0FBRyxFQUFxQixDQUFDO0lBQ2pELDRCQUE0QixHQUFHLElBQUksR0FBRyxFQUF1QyxDQUFDO0lBRTlFLE9BQU8sQ0FBcUI7SUFFN0MsWUFBcUIsV0FBbUIsRUFBVyxNQUEwQixHQUFHLEVBQUUsR0FBRyxDQUFDO1FBQWpFLGdCQUFXLEdBQVgsV0FBVyxDQUFRO1FBQVcsUUFBRyxHQUFILEdBQUcsQ0FBZ0M7UUFDckYsSUFBSSxDQUFDLE9BQU8sR0FBRyxFQUFFLENBQUMscUJBQXFCLENBQUMsSUFBSSx5QkFBeUIsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDO0lBQ3JGLENBQUM7SUFFRCxzQkFBc0IsQ0FBQyw0QkFBMEM7UUFFaEUsc0ZBQXNGO1FBRXRGLE1BQU0sS0FBSyxHQUFHLENBQUMsSUFBYSxFQUFRLEVBQUU7WUFDckMsSUFBSSxFQUFFLENBQUMsa0JBQWtCLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxFQUFFO2dCQUM5RCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsSUFBSSxJQUFJLElBQUksQ0FBQztnQkFDakMsTUFBTSxHQUFHLEdBQUcsR0FBRyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsUUFBUSxJQUFJLE1BQU0sQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDO2dCQUNwRSxJQUFJLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUU7b0JBQ3BDLE1BQU0sSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUM7aUJBQ3pCO2dCQUNELElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksU0FBUyxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQyxRQUFRLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQzthQUNwRjtZQUVELElBQUksRUFBRSxDQUFDLGtCQUFrQixDQUFDLElBQUksQ0FBQyxJQUFJLFdBQVcsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxhQUFhLENBQUMsRUFBRTtnQkFDbEYsSUFBSSxJQUFJLENBQUMsSUFBSSxFQUFFO29CQUNkLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUM7b0JBQ3pCLE1BQU0sR0FBRyxHQUFHLEdBQUcsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDLFFBQVEsSUFBSSxNQUFNLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQztvQkFDcEUsSUFBSSxJQUFJLENBQUMsNEJBQTRCLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxFQUFFO3dCQUMvQyxNQUFNLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO3FCQUN6QjtvQkFDRCxJQUFJLENBQUMsNEJBQTRCLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxJQUFJLGVBQWUsQ0FBQyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxDQUFDLENBQUM7aUJBQ3JHO2FBQ0Q7WUFFRCxJQUFJLEVBQUUsQ0FBQyxxQkFBcUIsQ0FBQyxJQUFJLENBQUM7bUJBQzlCLEVBQUUsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQzttQkFDNUIsV0FBVyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLGFBQWEsQ0FBQyxFQUNoRDtnQkFDRCxJQUFJLElBQUksQ0FBQyxJQUFJLElBQUksSUFBSSxDQUFDLElBQUksRUFBRSxFQUFFLDRDQUE0QztvQkFDekUsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQztvQkFDekIsTUFBTSxHQUFHLEdBQUcsR0FBRyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsUUFBUSxJQUFJLE1BQU0sQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDO29CQUNwRSxJQUFJLElBQUksQ0FBQyw0QkFBNEIsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUU7d0JBQy9DLE1BQU0sSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUM7cUJBQ3pCO29CQUNELElBQUksQ0FBQyw0QkFBNEIsQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksZUFBZSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQyxRQUFRLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQztpQkFDckc7YUFDRDtZQUVELElBQUksRUFBRSxDQUFDLG1CQUFtQixDQUFDLElBQUksQ0FBQzttQkFDNUIsRUFBRSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDO21CQUM1QixXQUFXLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsYUFBYSxDQUFDLEVBQ2hEO2dCQUNELEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLGVBQWUsQ0FBQyxZQUFZLEVBQUU7b0JBQ3JELE1BQU0sR0FBRyxHQUFHLEdBQUcsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDLFFBQVEsSUFBSSxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUM7b0JBQ3ZFLElBQUksSUFBSSxDQUFDLDRCQUE0QixDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRTt3QkFDL0MsTUFBTSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQztxQkFDekI7b0JBQ0QsSUFBSSxDQUFDLDRCQUE0QixDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsSUFBSSxTQUFTLENBQUMsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDLFFBQVEsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO2lCQUNuSDthQUNEO1lBRUQsRUFBRSxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDOUIsQ0FBQyxDQUFDO1FBRUYsS0FBSyxNQUFNLElBQUksSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsRUFBRyxDQUFDLGNBQWMsRUFBRSxFQUFFO1lBQy9ELElBQUksQ0FBQyxJQUFJLENBQUMsaUJBQWlCLEVBQUU7Z0JBQzVCLEVBQUUsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxDQUFDO2FBQzdCO1NBQ0Q7UUFDRCxJQUFJLENBQUMsR0FBRyxDQUFDLDZCQUE2QixJQUFJLENBQUMsaUJBQWlCLENBQUMsSUFBSSx3QkFBd0IsSUFBSSxDQUFDLDRCQUE0QixDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7UUFHbkkscUNBQXFDO1FBRXJDLE1BQU0sWUFBWSxHQUFHLENBQUMsSUFBZSxFQUFFLEVBQUU7WUFDeEMsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxlQUFlLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssS0FBSyxFQUFFLENBQUMsVUFBVSxDQUFDLGNBQWMsQ0FBQyxDQUFDO1lBQ3JHLElBQUksQ0FBQyxhQUFhLEVBQUU7Z0JBQ25CLG9CQUFvQjtnQkFDcEIsT0FBTzthQUNQO1lBRUQsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyx1QkFBdUIsQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLGFBQWEsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7WUFDN0csSUFBSSxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRTtnQkFDL0IsMkNBQTJDO2dCQUMzQyxPQUFPO2FBQ1A7WUFFRCxJQUFJLElBQUksQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFO2dCQUN0QixzQ0FBc0M7Z0JBQ3RDLE9BQU87YUFDUDtZQUVELE1BQU0sQ0FBQyxVQUFVLENBQUMsR0FBRyxJQUFJLENBQUM7WUFDMUIsTUFBTSxHQUFHLEdBQUcsR0FBRyxVQUFVLENBQUMsUUFBUSxJQUFJLFVBQVUsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDbEUsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUMvQyxJQUFJLENBQUMsTUFBTSxFQUFFO2dCQUNaLG1EQUFtRDtnQkFDbkQsT0FBTzthQUNQO1lBQ0QsTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUN2QixDQUFDLENBQUM7UUFDRixLQUFLLE1BQU0sSUFBSSxJQUFJLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLEVBQUUsRUFBRTtZQUNuRCxZQUFZLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDbkI7UUFFRCx1RUFBdUU7UUFDdkUsTUFBTSxVQUFVLEdBQUcsSUFBSSxHQUFHLEVBQW9CLENBQUM7UUFDL0MsSUFBSSxzQkFBc0IsR0FBRyxLQUFLLENBQUM7UUFDbkMsS0FBSyxNQUFNLElBQUksSUFBSSxJQUFJLENBQUMsaUJBQWlCLENBQUMsTUFBTSxFQUFFLEVBQUU7WUFDbkQsU0FBUyxDQUFDLGdDQUFnQyxDQUFDLElBQUksRUFBRSxDQUFDLElBQVksRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLEVBQUU7Z0JBQzVFLE1BQU0sR0FBRyxHQUFHLFVBQVUsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQ2pDLElBQUksR0FBRyxFQUFFO29CQUNSLEdBQUcsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUM7aUJBQ2Q7cUJBQU07b0JBQ04sVUFBVSxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO2lCQUM1QjtnQkFFRCxJQUFJLDRCQUE0QixJQUFJLENBQUMsNEJBQTRCLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFO29CQUM1RSxzQkFBc0IsR0FBRyxJQUFJLENBQUM7aUJBQzlCO1lBQ0YsQ0FBQyxDQUFDLENBQUM7U0FDSDtRQUNELEtBQUssTUFBTSxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsSUFBSSxVQUFVLEVBQUU7WUFDckMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxTQUFTLEdBQUcsOEJBQThCLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1NBQ3ZFO1FBQ0QsSUFBSSxzQkFBc0IsRUFBRTtZQUMzQixNQUFNLE9BQU8sR0FBRyxzSUFBc0ksQ0FBQztZQUN2SixJQUFJLENBQUMsR0FBRyxDQUFDLFVBQVUsT0FBTyxFQUFFLENBQUMsQ0FBQztZQUM5QixNQUFNLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1NBQ3pCO1FBRUQsaURBQWlEO1FBQ2pELEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLGlCQUFpQixDQUFDLE1BQU0sRUFBRSxFQUFFO1lBQ25ELFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsQ0FBQztTQUNsQztRQUNELElBQUksQ0FBQyxHQUFHLENBQUMsa0NBQWtDLENBQUMsQ0FBQztRQUk3QyxNQUFNLFdBQVcsR0FBRyxJQUFJLEdBQUcsRUFBa0IsQ0FBQztRQUU5QyxNQUFNLFVBQVUsR0FBRyxDQUFDLFFBQWdCLEVBQUUsSUFBVSxFQUFFLEVBQUU7WUFDbkQsTUFBTSxLQUFLLEdBQUcsV0FBVyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUN4QyxJQUFJLENBQUMsS0FBSyxFQUFFO2dCQUNYLFdBQVcsQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQzthQUNsQztpQkFBTTtnQkFDTixLQUFLLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO2FBQ2pCO1FBQ0YsQ0FBQyxDQUFDO1FBQ0YsTUFBTSxZQUFZLEdBQUcsQ0FBQyxPQUFlLEVBQUUsR0FBc0IsRUFBRSxFQUFFO1lBQ2hFLFVBQVUsQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFO2dCQUN4QixPQUFPLEVBQUUsQ0FBQyxHQUFHLENBQUMsVUFBVSxJQUFJLEVBQUUsQ0FBQyxHQUFHLE9BQU8sR0FBRyxDQUFDLEdBQUcsQ0FBQyxVQUFVLElBQUksRUFBRSxDQUFDO2dCQUNsRSxNQUFNLEVBQUUsR0FBRyxDQUFDLFFBQVEsQ0FBQyxLQUFLO2dCQUMxQixNQUFNLEVBQUUsR0FBRyxDQUFDLFFBQVEsQ0FBQyxNQUFNO2FBQzNCLENBQUMsQ0FBQztRQUNKLENBQUMsQ0FBQztRQUVGLEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLGlCQUFpQixDQUFDLE1BQU0sRUFBRSxFQUFFO1lBRW5ELElBQUksV0FBVyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxjQUFjLENBQUMsRUFBRTtnQkFDekQsU0FBUzthQUNUO1lBRUQsTUFBTSxFQUFFLEtBQUssTUFBTSxDQUFDLElBQUksRUFBRSxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFO2dCQUMvQyxJQUFJLENBQUMsU0FBUyxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUU7b0JBQ3hDLFNBQVMsTUFBTSxDQUFDO2lCQUNoQjtnQkFFRCxvREFBb0Q7Z0JBQ3BELHVEQUF1RDtnQkFDdkQsSUFBSSxNQUFNLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQztnQkFDekIsT0FBTyxNQUFNLEVBQUU7b0JBQ2QsSUFBSSxNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLDZCQUFxQixFQUFFO3dCQUN2RCxTQUFTLE1BQU0sQ0FBQztxQkFDaEI7b0JBQ0QsTUFBTSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7aUJBQ3ZCO2dCQUVELE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQzNDLE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO2dCQUN0RyxLQUFLLE1BQU0sR0FBRyxJQUFJLFNBQVMsRUFBRTtvQkFDNUIsWUFBWSxDQUFDLE9BQU8sRUFBRSxHQUFHLENBQUMsQ0FBQztpQkFDM0I7YUFDRDtTQUNEO1FBRUQsS0FBSyxNQUFNLElBQUksSUFBSSxJQUFJLENBQUMsNEJBQTRCLENBQUMsTUFBTSxFQUFFLEVBQUU7WUFDOUQsSUFBSSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLGVBQWUsQ0FBQyxFQUFFO2dCQUM3QyxTQUFTO2FBQ1Q7WUFFRCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsZUFBZSxDQUFDO1lBQ3JDLEtBQUssTUFBTSxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsSUFBSSxJQUFJLENBQUMsU0FBUyxFQUFFO2dCQUNsRCxNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLFFBQVEsRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7Z0JBQy9GLEtBQUssTUFBTSxHQUFHLElBQUksU0FBUyxFQUFFO29CQUM1QixZQUFZLENBQUMsT0FBTyxFQUFFLEdBQUcsQ0FBQyxDQUFDO2lCQUMzQjthQUNEO1NBQ0Q7UUFFRCxJQUFJLENBQUMsR0FBRyxDQUFDLHlCQUF5QixXQUFXLENBQUMsSUFBSSxRQUFRLENBQUMsQ0FBQztRQUU1RCwwQ0FBMEM7UUFDMUMsTUFBTSxNQUFNLEdBQUcsSUFBSSxHQUFHLEVBQXdCLENBQUM7UUFDL0MsSUFBSSxVQUFVLEdBQUcsQ0FBQyxDQUFDO1FBRW5CLEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUcsQ0FBQyxjQUFjLEVBQUUsRUFBRTtZQUUvRCxNQUFNLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFHLENBQUMsa0JBQWtCLEVBQUUsQ0FBQztZQUNoRixNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsQ0FBQztZQUNsRCxNQUFNLGFBQWEsR0FBRyxPQUFPLElBQUksSUFBQSxtQkFBYSxFQUFDLFVBQVUsSUFBSSxVQUFVLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUVwRixjQUFjO1lBQ2QsSUFBSSxTQUF5QyxDQUFDO1lBRTlDLElBQUksV0FBbUIsQ0FBQztZQUN4QixNQUFNLEtBQUssR0FBRyxXQUFXLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUM3QyxJQUFJLENBQUMsS0FBSyxFQUFFO2dCQUNYLFlBQVk7Z0JBQ1osV0FBVyxHQUFHLElBQUksQ0FBQyxXQUFXLEVBQUUsQ0FBQzthQUVqQztpQkFBTTtnQkFDTix1QkFBdUI7Z0JBQ3ZCLE1BQU0sZ0JBQWdCLEdBQUcsU0FBUyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsVUFBVSxFQUFFLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDO2dCQUM3RSxNQUFNLGNBQWMsR0FBRyxJQUFJLEdBQUcsRUFBcUIsQ0FBQztnQkFFcEQsZ0JBQWdCO2dCQUNoQixLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUM7Z0JBQzFDLE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxXQUFXLEVBQUUsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7Z0JBRWhELElBQUksUUFBMEIsQ0FBQztnQkFFL0IsS0FBSyxNQUFNLElBQUksSUFBSSxLQUFLLEVBQUU7b0JBQ3pCLElBQUksUUFBUSxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLE1BQU0sRUFBRTt3QkFDaEQsRUFBRTt3QkFDRixJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLE1BQU0sSUFBSSxRQUFRLENBQUMsT0FBTyxLQUFLLElBQUksQ0FBQyxPQUFPLEVBQUU7NEJBQ3pFLElBQUksQ0FBQyxHQUFHLENBQUMseUJBQXlCLEVBQUUsSUFBSSxDQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxDQUFDOzRCQUN2RSxNQUFNLElBQUksS0FBSyxDQUFDLGtCQUFrQixDQUFDLENBQUM7eUJBQ3BDOzZCQUFNOzRCQUNOLFNBQVM7eUJBQ1Q7cUJBQ0Q7b0JBQ0QsUUFBUSxHQUFHLElBQUksQ0FBQztvQkFDaEIsTUFBTSxXQUFXLEdBQUcsVUFBVSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQztvQkFDdkYsVUFBVSxJQUFJLFdBQVcsQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7b0JBRXZELGNBQWM7b0JBQ2QsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLDZCQUE2QixDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQztvQkFHNUQsSUFBSSxRQUFRLEdBQUcsY0FBYyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUM7b0JBQzVDLElBQUksQ0FBQyxRQUFRLEVBQUU7d0JBQ2QsUUFBUSxHQUFHLEVBQUUsQ0FBQzt3QkFDZCxjQUFjLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLENBQUM7cUJBQ3ZDO29CQUNELFFBQVEsQ0FBQyxPQUFPLENBQUM7d0JBQ2hCLE1BQU0sRUFBRSxnQkFBZ0I7d0JBQ3hCLFFBQVEsRUFBRSxFQUFFLElBQUksRUFBRSxHQUFHLENBQUMsSUFBSSxHQUFHLENBQUMsRUFBRSxNQUFNLEVBQUUsR0FBRyxDQUFDLFNBQVMsRUFBRTt3QkFDdkQsU0FBUyxFQUFFLEVBQUUsSUFBSSxFQUFFLEdBQUcsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxFQUFFLE1BQU0sRUFBRSxHQUFHLENBQUMsU0FBUyxFQUFFO3dCQUN4RCxJQUFJLEVBQUUsV0FBVztxQkFDakIsRUFBRTt3QkFDRixNQUFNLEVBQUUsZ0JBQWdCO3dCQUN4QixRQUFRLEVBQUUsRUFBRSxJQUFJLEVBQUUsR0FBRyxDQUFDLElBQUksR0FBRyxDQUFDLEVBQUUsTUFBTSxFQUFFLEdBQUcsQ0FBQyxTQUFTLEdBQUcsSUFBSSxDQUFDLE1BQU0sRUFBRTt3QkFDckUsU0FBUyxFQUFFLEVBQUUsSUFBSSxFQUFFLEdBQUcsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxFQUFFLE1BQU0sRUFBRSxHQUFHLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFO3FCQUM5RSxDQUFDLENBQUM7aUJBQ0g7Z0JBRUQsb0VBQW9FO2dCQUNwRSxTQUFTLEdBQUcsSUFBSSwrQkFBa0IsQ0FBQyxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxVQUFVLEVBQUUsYUFBYSxFQUFFLENBQUMsQ0FBQztnQkFDdEcsU0FBUyxDQUFDLGdCQUFnQixDQUFDLGdCQUFnQixFQUFFLElBQUksQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO2dCQUNqRSxLQUFLLE1BQU0sQ0FBQyxFQUFFLFFBQVEsQ0FBQyxJQUFJLGNBQWMsRUFBRTtvQkFDMUMsSUFBSSxTQUFTLEdBQUcsQ0FBQyxDQUFDO29CQUNsQixLQUFLLE1BQU0sT0FBTyxJQUFJLFFBQVEsRUFBRTt3QkFDL0IsU0FBUyxDQUFDLFVBQVUsQ0FBQzs0QkFDcEIsR0FBRyxPQUFPOzRCQUNWLFNBQVMsRUFBRSxFQUFFLElBQUksRUFBRSxPQUFPLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLFNBQVMsQ0FBQyxNQUFNLEdBQUcsU0FBUyxFQUFFO3lCQUN6RixDQUFDLENBQUM7d0JBQ0gsU0FBUyxJQUFJLE9BQU8sQ0FBQyxRQUFRLENBQUMsTUFBTSxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDO3FCQUNoRTtpQkFDRDtnQkFFRCxXQUFXLEdBQUcsVUFBVSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQzthQUNsQztZQUNELE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxFQUFFLEdBQUcsRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7U0FDbEY7UUFFRCxJQUFJLENBQUMsR0FBRyxDQUFDLFNBQVMsVUFBVSxHQUFHLElBQUksVUFBVSxDQUFDLENBQUM7UUFDL0MsT0FBTyxNQUFNLENBQUM7SUFDZixDQUFDO0NBQ0Q7QUFuU0QsMEJBbVNDO0FBRUQsZ0JBQWdCO0FBRWhCLFNBQVMsV0FBVyxDQUFDLElBQWEsRUFBRSxJQUFtQjtJQUN0RCxNQUFNLFNBQVMsR0FBRyxFQUFFLENBQUMsZ0JBQWdCLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztJQUNoRixPQUFPLE9BQU8sQ0FBQyxTQUFTLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLElBQUksS0FBSyxJQUFJLENBQUMsQ0FBQyxDQUFDO0FBQzdELENBQUM7QUFFRCxTQUFTLFNBQVMsQ0FBQyxJQUFZO0lBQzlCLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDakMsQ0FBQztBQUVELEtBQUssVUFBVSxJQUFJO0lBRWxCLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFFLHlCQUF5QixDQUFDLENBQUM7SUFDcEUsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsQ0FBQztJQUM5QyxNQUFNLGNBQWMsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxXQUFXLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQztJQUU5RixFQUFFLENBQUMsTUFBTSxDQUFDLFdBQVcsRUFBRSxjQUFjLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUU1RCxJQUFJLEtBQUssRUFBRSxNQUFNLENBQUMsUUFBUSxFQUFFLFFBQVEsQ0FBQyxJQUFJLElBQUksT0FBTyxDQUFDLFdBQVcsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsc0JBQXNCLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLEVBQUU7UUFDOUgsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxjQUFjLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxXQUFXLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQztRQUNwRixNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUN4RSxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLFdBQVcsRUFBRSxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDdkQsSUFBSSxRQUFRLENBQUMsU0FBUyxFQUFFO1lBQ3ZCLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsV0FBVyxHQUFHLE1BQU0sRUFBRSxRQUFRLENBQUMsU0FBUyxDQUFDLENBQUM7U0FDdEU7S0FDRDtBQUNGLENBQUM7QUFFRCxJQUFJLFVBQVUsS0FBSyxjQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUU7SUFDM0IsSUFBSSxFQUFFLENBQUM7Q0FDUCJ9 \ No newline at end of file diff --git a/code/build/lib/monaco-api.js b/code/build/lib/monaco-api.js index 6512b6ae886..2052806c46b 100644 --- a/code/build/lib/monaco-api.js +++ b/code/build/lib/monaco-api.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.execute = exports.run3 = exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; +exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; +exports.run3 = run3; +exports.execute = execute; const fs = require("fs"); const path = require("path"); const fancyLog = require("fancy-log"); @@ -559,7 +561,6 @@ function run3(resolver) { const sourceFileGetter = (moduleId) => resolver.getDeclarationSourceFile(moduleId); return _run(resolver.ts, sourceFileGetter); } -exports.run3 = run3; class TypeScriptLanguageServiceHost { _ts; _libs; @@ -623,5 +624,4 @@ function execute() { } return r; } -exports.execute = execute; //# sourceMappingURL=monaco-api.js.map \ No newline at end of file diff --git a/code/build/lib/nls.js b/code/build/lib/nls.js index 982f74bcf4d..48ca84f2433 100644 --- a/code/build/lib/nls.js +++ b/code/build/lib/nls.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.nls = void 0; +exports.nls = nls; const lazy = require("lazy.js"); const event_stream_1 = require("event-stream"); const File = require("vinyl"); @@ -74,7 +74,6 @@ function nls() { })); return (0, event_stream_1.duplex)(input, output); } -exports.nls = nls; function isImportNode(ts, node) { return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; } diff --git a/code/build/lib/optimize.js b/code/build/lib/optimize.js index 9dff0859acc..237f2bc20e8 100644 --- a/code/build/lib/optimize.js +++ b/code/build/lib/optimize.js @@ -4,7 +4,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.minifyTask = exports.optimizeTask = exports.optimizeLoaderTask = exports.loaderConfig = void 0; +exports.loaderConfig = loaderConfig; +exports.optimizeLoaderTask = optimizeLoaderTask; +exports.optimizeTask = optimizeTask; +exports.minifyTask = minifyTask; const es = require("event-stream"); const gulp = require("gulp"); const concat = require("gulp-concat"); @@ -33,7 +36,6 @@ function loaderConfig() { result['vs/css'] = { inlineResources: true }; return result; } -exports.loaderConfig = loaderConfig; const IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; function loaderPlugin(src, base, amdModuleId) { return (gulp @@ -223,7 +225,6 @@ function optimizeManualTask(options) { function optimizeLoaderTask(src, out, bundleLoader, bundledFileHeader = '', externalLoaderInfo) { return () => loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo).pipe(gulp.dest(out)); } -exports.optimizeLoaderTask = optimizeLoaderTask; function optimizeTask(opts) { return function () { const optimizers = [optimizeAMDTask(opts.amd)]; @@ -236,7 +237,6 @@ function optimizeTask(opts) { return es.merge(...optimizers).pipe(gulp.dest(opts.out)); }; } -exports.optimizeTask = optimizeTask; function minifyTask(src, sourceMapBaseUrl) { const esbuild = require('esbuild'); const sourceMappingURL = sourceMapBaseUrl ? ((f) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; @@ -284,5 +284,4 @@ function minifyTask(src, sourceMapBaseUrl) { }), gulp.dest(src + '-min'), (err) => cb(err)); }; } -exports.minifyTask = minifyTask; //# sourceMappingURL=optimize.js.map \ No newline at end of file diff --git a/code/build/lib/reporter.js b/code/build/lib/reporter.js index 305d7364287..9d4a1b4fd79 100644 --- a/code/build/lib/reporter.js +++ b/code/build/lib/reporter.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createReporter = void 0; +exports.createReporter = createReporter; const es = require("event-stream"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); @@ -99,5 +99,4 @@ function createReporter(id) { }; return result; } -exports.createReporter = createReporter; //# sourceMappingURL=reporter.js.map \ No newline at end of file diff --git a/code/build/lib/standalone.js b/code/build/lib/standalone.js index 4ddf88ed223..dbc47db0833 100644 --- a/code/build/lib/standalone.js +++ b/code/build/lib/standalone.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createESMSourcesAndResources2 = exports.extractEditor = void 0; +exports.extractEditor = extractEditor; +exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; const fs = require("fs"); const path = require("path"); const tss = require("./treeshaking"); @@ -111,7 +112,6 @@ function extractEditor(options) { 'vs/nls.mock.ts', ].forEach(copyFile); } -exports.extractEditor = extractEditor; function createESMSourcesAndResources2(options) { const ts = require('typescript'); const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); @@ -251,7 +251,6 @@ function createESMSourcesAndResources2(options) { } } } -exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; function transportCSS(module, enqueue, write) { if (!/\.css/.test(module)) { return false; diff --git a/code/build/lib/stats.js b/code/build/lib/stats.js index d923bb809da..e089cb0c1b4 100644 --- a/code/build/lib/stats.js +++ b/code/build/lib/stats.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createStatsStream = void 0; +exports.createStatsStream = createStatsStream; const es = require("event-stream"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); @@ -73,5 +73,4 @@ function createStatsStream(group, log) { this.emit('end'); }); } -exports.createStatsStream = createStatsStream; //# sourceMappingURL=stats.js.map \ No newline at end of file diff --git a/code/build/lib/stylelint/validateVariableNames.js b/code/build/lib/stylelint/validateVariableNames.js index 2367fb94c2e..57b2aad957f 100644 --- a/code/build/lib/stylelint/validateVariableNames.js +++ b/code/build/lib/stylelint/validateVariableNames.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVariableNameValidator = void 0; +exports.getVariableNameValidator = getVariableNameValidator; const fs_1 = require("fs"); const path = require("path"); const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; @@ -30,5 +30,4 @@ function getVariableNameValidator() { } }; } -exports.getVariableNameValidator = getVariableNameValidator; //# sourceMappingURL=validateVariableNames.js.map \ No newline at end of file diff --git a/code/build/lib/stylelint/vscode-known-variables.json b/code/build/lib/stylelint/vscode-known-variables.json index e9ec91f29ea..eca0468a14c 100644 --- a/code/build/lib/stylelint/vscode-known-variables.json +++ b/code/build/lib/stylelint/vscode-known-variables.json @@ -15,6 +15,8 @@ "--vscode-activityBarTop-dropBorder", "--vscode-activityBarTop-foreground", "--vscode-activityBarTop-inactiveForeground", + "--vscode-activityBarTop-background", + "--vscode-activityBarTop-activeBackground", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", @@ -43,8 +45,8 @@ "--vscode-charts-yellow", "--vscode-chat-avatarBackground", "--vscode-chat-avatarForeground", - "--vscode-chat-requestBorder", "--vscode-chat-requestBackground", + "--vscode-chat-requestBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", "--vscode-chat-list-background", @@ -560,6 +562,7 @@ "--vscode-sideBarSectionHeader-border", "--vscode-sideBarSectionHeader-foreground", "--vscode-sideBarTitle-foreground", + "--vscode-sideBarActivityBarTop-border", "--vscode-sideBySideEditor-horizontalBorder", "--vscode-sideBySideEditor-verticalBorder", "--vscode-simpleFindWidget-sashBorder", @@ -694,7 +697,6 @@ "--vscode-terminalOverviewRuler-findMatchForeground", "--vscode-terminalStickyScroll-background", "--vscode-terminalStickyScrollHover-background", - "--vscode-testing-coverage-lineHeight", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", "--vscode-testing-coveredBackground", @@ -750,8 +752,7 @@ "--vscode-widget-border", "--vscode-widget-shadow", "--vscode-window-activeBorder", - "--vscode-window-inactiveBorder", - "--vscode-multiDiffEditor-headerBackground" + "--vscode-window-inactiveBorder" ], "others": [ "--background-dark", @@ -792,8 +793,6 @@ "--vscode-hover-maxWidth", "--vscode-hover-sourceWhiteSpace", "--vscode-hover-whiteSpace", - "--vscode-inline-chat-cropped", - "--vscode-inline-chat-expanded", "--vscode-inline-chat-quick-voice-height", "--vscode-inline-chat-quick-voice-width", "--vscode-editor-dictation-widget-height", diff --git a/code/build/lib/task.js b/code/build/lib/task.js index 6b040a75698..597b2a0d397 100644 --- a/code/build/lib/task.js +++ b/code/build/lib/task.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.define = exports.parallel = exports.series = void 0; +exports.series = series; +exports.parallel = parallel; +exports.define = define; const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); function _isPromise(p) { @@ -67,7 +69,6 @@ function series(...tasks) { result._tasks = tasks; return result; } -exports.series = series; function parallel(...tasks) { const result = async () => { await Promise.all(tasks.map(t => _execute(t))); @@ -75,7 +76,6 @@ function parallel(...tasks) { result._tasks = tasks; return result; } -exports.parallel = parallel; function define(name, task) { if (task._tasks) { // This is a composite task @@ -94,5 +94,4 @@ function define(name, task) { task.displayName = name; return task; } -exports.define = define; //# sourceMappingURL=task.js.map \ No newline at end of file diff --git a/code/build/lib/treeshaking.js b/code/build/lib/treeshaking.js index 51c610ecda2..c8e95511877 100644 --- a/code/build/lib/treeshaking.js +++ b/code/build/lib/treeshaking.js @@ -4,7 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.shake = exports.toStringShakeLevel = exports.ShakeLevel = void 0; +exports.ShakeLevel = void 0; +exports.toStringShakeLevel = toStringShakeLevel; +exports.shake = shake; const fs = require("fs"); const path = require("path"); const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); @@ -24,7 +26,6 @@ function toStringShakeLevel(shakeLevel) { return 'ClassMembers (2)'; } } -exports.toStringShakeLevel = toStringShakeLevel; function printDiagnostics(options, diagnostics) { for (const diag of diagnostics) { let result = ''; @@ -61,7 +62,6 @@ function shake(options) { markNodes(ts, languageService, options); return generateResult(ts, languageService, options.shakeLevel); } -exports.shake = shake; //#region Discovery, LanguageService & Setup function createTypeScriptLanguageService(ts, options) { // Discover referenced files diff --git a/code/build/lib/tsb/builder.js b/code/build/lib/tsb/builder.js index e87945ea9cc..fc74bfa8acc 100644 --- a/code/build/lib/tsb/builder.js +++ b/code/build/lib/tsb/builder.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.createTypeScriptBuilder = exports.CancellationToken = void 0; +exports.CancellationToken = void 0; +exports.createTypeScriptBuilder = createTypeScriptBuilder; const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); @@ -364,7 +365,6 @@ function createTypeScriptBuilder(config, projectFile, cmd) { languageService: service }; } -exports.createTypeScriptBuilder = createTypeScriptBuilder; class ScriptSnapshot { _text; _mtime; diff --git a/code/build/lib/tsb/index.js b/code/build/lib/tsb/index.js index 47f26bc8178..8b8116d5a49 100644 --- a/code/build/lib/tsb/index.js +++ b/code/build/lib/tsb/index.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.create = void 0; +exports.create = create; const Vinyl = require("vinyl"); const through = require("through"); const builder = require("./builder"); @@ -132,5 +132,4 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) }; return result; } -exports.create = create; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/code/build/lib/util.js b/code/build/lib/util.js index 388ef5df948..ed52776c2c0 100644 --- a/code/build/lib/util.js +++ b/code/build/lib/util.js @@ -4,7 +4,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildWebNodePaths = exports.createExternalLoaderConfig = exports.acquireWebNodePaths = exports.getElectronVersion = exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.ensureDir = exports.rreddir = exports.rimraf = exports.rewriteSourceMappingURL = exports.appendOwnPathSourceURL = exports.$if = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.debounce = exports.incremental = void 0; +exports.incremental = incremental; +exports.debounce = debounce; +exports.fixWin32DirectoryPermissions = fixWin32DirectoryPermissions; +exports.setExecutableBit = setExecutableBit; +exports.toFileUri = toFileUri; +exports.skipDirectories = skipDirectories; +exports.cleanNodeModules = cleanNodeModules; +exports.loadSourcemaps = loadSourcemaps; +exports.stripSourceMappingURL = stripSourceMappingURL; +exports.$if = $if; +exports.appendOwnPathSourceURL = appendOwnPathSourceURL; +exports.rewriteSourceMappingURL = rewriteSourceMappingURL; +exports.rimraf = rimraf; +exports.rreddir = rreddir; +exports.ensureDir = ensureDir; +exports.rebase = rebase; +exports.filter = filter; +exports.versionStringToNumber = versionStringToNumber; +exports.streamToPromise = streamToPromise; +exports.getElectronVersion = getElectronVersion; +exports.acquireWebNodePaths = acquireWebNodePaths; +exports.createExternalLoaderConfig = createExternalLoaderConfig; +exports.buildWebNodePaths = buildWebNodePaths; const es = require("event-stream"); const _debounce = require("debounce"); const _filter = require("gulp-filter"); @@ -54,7 +76,6 @@ function incremental(streamProvider, initial, supportsCancellation) { }); return es.duplex(input, output); } -exports.incremental = incremental; function debounce(task, duration = 500) { const input = es.through(); const output = es.through(); @@ -83,7 +104,6 @@ function debounce(task, duration = 500) { }); return es.duplex(input, output); } -exports.debounce = debounce; function fixWin32DirectoryPermissions() { if (!/win32/.test(process.platform)) { return es.through(); @@ -95,7 +115,6 @@ function fixWin32DirectoryPermissions() { return f; }); } -exports.fixWin32DirectoryPermissions = fixWin32DirectoryPermissions; function setExecutableBit(pattern) { const setBit = es.mapSync(f => { if (!f.stat) { @@ -115,7 +134,6 @@ function setExecutableBit(pattern) { .pipe(filter.restore); return es.duplex(input, output); } -exports.setExecutableBit = setExecutableBit; function toFileUri(filePath) { const match = filePath.match(/^([a-z])\:(.*)$/i); if (match) { @@ -123,7 +141,6 @@ function toFileUri(filePath) { } return 'file://' + filePath.replace(/\\/g, '/'); } -exports.toFileUri = toFileUri; function skipDirectories() { return es.mapSync(f => { if (!f.isDirectory()) { @@ -131,7 +148,6 @@ function skipDirectories() { } }); } -exports.skipDirectories = skipDirectories; function cleanNodeModules(rulePath) { const rules = fs.readFileSync(rulePath, 'utf8') .split(/\r?\n/g) @@ -143,7 +159,6 @@ function cleanNodeModules(rulePath) { const output = es.merge(input.pipe(_filter(['**', ...excludes])), input.pipe(_filter(includes))); return es.duplex(input, output); } -exports.cleanNodeModules = cleanNodeModules; function loadSourcemaps() { const input = es.through(); const output = input @@ -185,7 +200,6 @@ function loadSourcemaps() { })); return es.duplex(input, output); } -exports.loadSourcemaps = loadSourcemaps; function stripSourceMappingURL() { const input = es.through(); const output = input @@ -196,7 +210,6 @@ function stripSourceMappingURL() { })); return es.duplex(input, output); } -exports.stripSourceMappingURL = stripSourceMappingURL; /** Splits items in the stream based on the predicate, sending them to onTrue if true, or onFalse otherwise */ function $if(test, onTrue, onFalse = es.through()) { if (typeof test === 'boolean') { @@ -204,7 +217,6 @@ function $if(test, onTrue, onFalse = es.through()) { } return ternaryStream(test, onTrue, onFalse); } -exports.$if = $if; /** Operator that appends the js files' original path a sourceURL, so debug locations map */ function appendOwnPathSourceURL() { const input = es.through(); @@ -218,7 +230,6 @@ function appendOwnPathSourceURL() { })); return es.duplex(input, output); } -exports.appendOwnPathSourceURL = appendOwnPathSourceURL; function rewriteSourceMappingURL(sourceMappingURLBase) { const input = es.through(); const output = input @@ -230,7 +241,6 @@ function rewriteSourceMappingURL(sourceMappingURLBase) { })); return es.duplex(input, output); } -exports.rewriteSourceMappingURL = rewriteSourceMappingURL; function rimraf(dir) { const result = () => new Promise((c, e) => { let retries = 0; @@ -250,7 +260,6 @@ function rimraf(dir) { result.taskName = `clean-${path.basename(dir).toLowerCase()}`; return result; } -exports.rimraf = rimraf; function _rreaddir(dirPath, prepend, result) { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { @@ -267,7 +276,6 @@ function rreddir(dirPath) { _rreaddir(dirPath, '', result); return result; } -exports.rreddir = rreddir; function ensureDir(dirPath) { if (fs.existsSync(dirPath)) { return; @@ -275,14 +283,12 @@ function ensureDir(dirPath) { ensureDir(path.dirname(dirPath)); fs.mkdirSync(dirPath); } -exports.ensureDir = ensureDir; function rebase(count) { return rename(f => { const parts = f.dirname ? f.dirname.split(/[\/\\]/) : []; f.dirname = parts.slice(count).join(path.sep); }); } -exports.rebase = rebase; function filter(fn) { const result = es.through(function (data) { if (fn(data)) { @@ -295,7 +301,6 @@ function filter(fn) { result.restore = es.through(); return result; } -exports.filter = filter; function versionStringToNumber(versionStr) { const semverRegex = /(\d+)\.(\d+)\.(\d+)/; const match = versionStr.match(semverRegex); @@ -304,21 +309,18 @@ function versionStringToNumber(versionStr) { } return parseInt(match[1], 10) * 1e4 + parseInt(match[2], 10) * 1e2 + parseInt(match[3], 10); } -exports.versionStringToNumber = versionStringToNumber; function streamToPromise(stream) { return new Promise((c, e) => { stream.on('error', err => e(err)); stream.on('end', () => c()); }); } -exports.streamToPromise = streamToPromise; function getElectronVersion() { const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); const electronVersion = /^target "(.*)"$/m.exec(yarnrc)[1]; const msBuildId = /^ms_build_id "(.*)"$/m.exec(yarnrc)[1]; return { electronVersion, msBuildId }; } -exports.getElectronVersion = getElectronVersion; function acquireWebNodePaths() { const root = path.join(__dirname, '..', '..'); const webPackageJSON = path.join(root, '/remote/web', 'package.json'); @@ -367,7 +369,6 @@ function acquireWebNodePaths() { nodePaths['@microsoft/applicationinsights-core-js'] = 'browser/applicationinsights-core-js.min.js'; return nodePaths; } -exports.acquireWebNodePaths = acquireWebNodePaths; function createExternalLoaderConfig(webEndpoint, commit, quality) { if (!webEndpoint || !commit || !quality) { return undefined; @@ -384,7 +385,6 @@ function createExternalLoaderConfig(webEndpoint, commit, quality) { }; return externalLoaderConfig; } -exports.createExternalLoaderConfig = createExternalLoaderConfig; function buildWebNodePaths(outDir) { const result = () => new Promise((resolve, _) => { const root = path.join(__dirname, '..', '..'); @@ -405,5 +405,4 @@ function buildWebNodePaths(outDir) { result.taskName = 'build-web-node-paths'; return result; } -exports.buildWebNodePaths = buildWebNodePaths; //# sourceMappingURL=util.js.map \ No newline at end of file diff --git a/code/build/linux/debian/calculate-deps.js b/code/build/linux/debian/calculate-deps.js index 6304df9edda..bbcb6bfc3de 100644 --- a/code/build/linux/debian/calculate-deps.js +++ b/code/build/linux/debian/calculate-deps.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePackageDeps = void 0; +exports.generatePackageDeps = generatePackageDeps; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const os_1 = require("os"); @@ -17,7 +17,6 @@ function generatePackageDeps(files, arch, chromiumSysroot, vscodeSysroot) { dependencies.push(additionalDepsSet); return dependencies; } -exports.generatePackageDeps = generatePackageDeps; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/calculate_package_deps.py. function calculatePackageDeps(binaryPath, arch, chromiumSysroot, vscodeSysroot) { try { diff --git a/code/build/linux/debian/install-sysroot.js b/code/build/linux/debian/install-sysroot.js index d637fce3ca6..feca7d3fa9d 100644 --- a/code/build/linux/debian/install-sysroot.js +++ b/code/build/linux/debian/install-sysroot.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getChromiumSysroot = exports.getVSCodeSysroot = void 0; +exports.getVSCodeSysroot = getVSCodeSysroot; +exports.getChromiumSysroot = getChromiumSysroot; const child_process_1 = require("child_process"); const os_1 = require("os"); const fs = require("fs"); @@ -156,7 +157,6 @@ async function getVSCodeSysroot(arch) { fs.writeFileSync(stamp, expectedName); return result; } -exports.getVSCodeSysroot = getVSCodeSysroot; async function getChromiumSysroot(arch) { const sysrootJSONUrl = `https://raw.githubusercontent.com/electron/electron/v${getElectronVersion().electronVersion}/script/sysroots.json`; const sysrootDictLocation = `${(0, os_1.tmpdir)()}/sysroots.json`; @@ -214,5 +214,4 @@ async function getChromiumSysroot(arch) { fs.writeFileSync(stamp, url); return sysroot; } -exports.getChromiumSysroot = getChromiumSysroot; //# sourceMappingURL=install-sysroot.js.map \ No newline at end of file diff --git a/code/build/linux/debian/types.js b/code/build/linux/debian/types.js index 2cd177c34a8..ce21d50e1a9 100644 --- a/code/build/linux/debian/types.js +++ b/code/build/linux/debian/types.js @@ -4,9 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.isDebianArchString = void 0; +exports.isDebianArchString = isDebianArchString; function isDebianArchString(s) { return ['amd64', 'armhf', 'arm64'].includes(s); } -exports.isDebianArchString = isDebianArchString; //# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/code/build/linux/dependencies-generator.js b/code/build/linux/dependencies-generator.js index e40ed70901c..80c247d1129 100644 --- a/code/build/linux/dependencies-generator.js +++ b/code/build/linux/dependencies-generator.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getDependencies = void 0; +exports.getDependencies = getDependencies; const child_process_1 = require("child_process"); const path = require("path"); const install_sysroot_1 = require("./debian/install-sysroot"); @@ -23,7 +23,7 @@ const product = require("../../product.json"); // The reference dependencies, which one has to update when the new dependencies // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/118.0.5993.159:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ @@ -92,7 +92,6 @@ async function getDependencies(packageType, buildDir, applicationName, arch) { } return sortedDependencies; } -exports.getDependencies = getDependencies; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py. function mergePackageDeps(inputDeps) { const requires = new Set(); diff --git a/code/build/linux/dependencies-generator.ts b/code/build/linux/dependencies-generator.ts index 12bc3c08a64..9f1a068b8d7 100644 --- a/code/build/linux/dependencies-generator.ts +++ b/code/build/linux/dependencies-generator.ts @@ -25,7 +25,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/118.0.5993.159:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/code/build/linux/libcxx-fetcher.js b/code/build/linux/libcxx-fetcher.js index 1e195ba1fac..cfdc9498502 100644 --- a/code/build/linux/libcxx-fetcher.js +++ b/code/build/linux/libcxx-fetcher.js @@ -4,7 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.downloadLibcxxObjects = exports.downloadLibcxxHeaders = void 0; +exports.downloadLibcxxHeaders = downloadLibcxxHeaders; +exports.downloadLibcxxObjects = downloadLibcxxObjects; // Can be removed once https://github.com/electron/electron-rebuild/pull/703 is available. const fs = require("fs"); const path = require("path"); @@ -29,7 +30,6 @@ async function downloadLibcxxHeaders(outDir, electronVersion, lib_name) { d(`unpacking ${lib_name}_headers from ${headers}`); await extract(headers, { dir: outDir }); } -exports.downloadLibcxxHeaders = downloadLibcxxHeaders; async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64') { if (await fs.existsSync(path.resolve(outDir, 'libc++.a'))) { return; @@ -47,7 +47,6 @@ async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64' d(`unpacking libcxx-objects from ${objects}`); await extract(objects, { dir: outDir }); } -exports.downloadLibcxxObjects = downloadLibcxxObjects; async function main() { const libcxxObjectsDirPath = process.env['VSCODE_LIBCXX_OBJECTS_DIR']; const libcxxHeadersDownloadDir = process.env['VSCODE_LIBCXX_HEADERS_DIR']; diff --git a/code/build/linux/rpm/calculate-deps.js b/code/build/linux/rpm/calculate-deps.js index ac870e4a546..b19e26f1854 100644 --- a/code/build/linux/rpm/calculate-deps.js +++ b/code/build/linux/rpm/calculate-deps.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePackageDeps = void 0; +exports.generatePackageDeps = generatePackageDeps; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const dep_lists_1 = require("./dep-lists"); @@ -14,7 +14,6 @@ function generatePackageDeps(files) { dependencies.push(additionalDepsSet); return dependencies; } -exports.generatePackageDeps = generatePackageDeps; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/calculate_package_deps.py. function calculatePackageDeps(binaryPath) { try { diff --git a/code/build/linux/rpm/dep-lists.js b/code/build/linux/rpm/dep-lists.js index b9a6e80d5f3..bd84fc146dc 100644 --- a/code/build/linux/rpm/dep-lists.js +++ b/code/build/linux/rpm/dep-lists.js @@ -81,7 +81,6 @@ exports.referenceGeneratedDepsByArch = { 'libnss3.so(NSS_3.11)(64bit)', 'libnss3.so(NSS_3.12)(64bit)', 'libnss3.so(NSS_3.12.1)(64bit)', - 'libnss3.so(NSS_3.13)(64bit)', 'libnss3.so(NSS_3.2)(64bit)', 'libnss3.so(NSS_3.22)(64bit)', 'libnss3.so(NSS_3.3)(64bit)', @@ -173,7 +172,6 @@ exports.referenceGeneratedDepsByArch = { 'libnss3.so(NSS_3.11)', 'libnss3.so(NSS_3.12)', 'libnss3.so(NSS_3.12.1)', - 'libnss3.so(NSS_3.13)', 'libnss3.so(NSS_3.2)', 'libnss3.so(NSS_3.22)', 'libnss3.so(NSS_3.22)(64bit)', @@ -269,7 +267,6 @@ exports.referenceGeneratedDepsByArch = { 'libnss3.so(NSS_3.11)(64bit)', 'libnss3.so(NSS_3.12)(64bit)', 'libnss3.so(NSS_3.12.1)(64bit)', - 'libnss3.so(NSS_3.13)(64bit)', 'libnss3.so(NSS_3.2)(64bit)', 'libnss3.so(NSS_3.22)(64bit)', 'libnss3.so(NSS_3.3)(64bit)', diff --git a/code/build/linux/rpm/dep-lists.ts b/code/build/linux/rpm/dep-lists.ts index 275d88b95a8..82a4fe7698d 100644 --- a/code/build/linux/rpm/dep-lists.ts +++ b/code/build/linux/rpm/dep-lists.ts @@ -80,7 +80,6 @@ export const referenceGeneratedDepsByArch = { 'libnss3.so(NSS_3.11)(64bit)', 'libnss3.so(NSS_3.12)(64bit)', 'libnss3.so(NSS_3.12.1)(64bit)', - 'libnss3.so(NSS_3.13)(64bit)', 'libnss3.so(NSS_3.2)(64bit)', 'libnss3.so(NSS_3.22)(64bit)', 'libnss3.so(NSS_3.3)(64bit)', @@ -172,7 +171,6 @@ export const referenceGeneratedDepsByArch = { 'libnss3.so(NSS_3.11)', 'libnss3.so(NSS_3.12)', 'libnss3.so(NSS_3.12.1)', - 'libnss3.so(NSS_3.13)', 'libnss3.so(NSS_3.2)', 'libnss3.so(NSS_3.22)', 'libnss3.so(NSS_3.22)(64bit)', @@ -268,7 +266,6 @@ export const referenceGeneratedDepsByArch = { 'libnss3.so(NSS_3.11)(64bit)', 'libnss3.so(NSS_3.12)(64bit)', 'libnss3.so(NSS_3.12.1)(64bit)', - 'libnss3.so(NSS_3.13)(64bit)', 'libnss3.so(NSS_3.2)(64bit)', 'libnss3.so(NSS_3.22)(64bit)', 'libnss3.so(NSS_3.3)(64bit)', diff --git a/code/build/linux/rpm/types.js b/code/build/linux/rpm/types.js index 6dba7cf38d1..a20b9c2fe02 100644 --- a/code/build/linux/rpm/types.js +++ b/code/build/linux/rpm/types.js @@ -4,9 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.isRpmArchString = void 0; +exports.isRpmArchString = isRpmArchString; function isRpmArchString(s) { return ['x86_64', 'armv7hl', 'aarch64'].includes(s); } -exports.isRpmArchString = isRpmArchString; //# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/code/build/win32/explorer-appx-fetcher.js b/code/build/win32/explorer-appx-fetcher.js index d618c21674a..554b449d872 100644 --- a/code/build/win32/explorer-appx-fetcher.js +++ b/code/build/win32/explorer-appx-fetcher.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.downloadExplorerAppx = void 0; +exports.downloadExplorerAppx = downloadExplorerAppx; const fs = require("fs"); const debug = require("debug"); const extract = require("extract-zip"); @@ -36,7 +36,6 @@ async function downloadExplorerAppx(outDir, quality = 'stable', targetArch = 'x6 d(`unpacking from ${fileName}`); await extract(artifact, { dir: fs.realpathSync(outDir) }); } -exports.downloadExplorerAppx = downloadExplorerAppx; async function main(outputDir) { const arch = process.env['VSCODE_ARCH']; if (!outputDir) { diff --git a/code/cgmanifest.json b/code/cgmanifest.json index 2673931fdb6..19734908f1a 100644 --- a/code/cgmanifest.json +++ b/code/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "b1f5594cf472956192e71c38ebfc22472d44a03d" + "commitHash": "14d11e5bb9b5b1cd51f7b19546e74a73cab42084" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "118.0.5993.159" + "version": "120.0.6099.291" }, { "component": { @@ -48,7 +48,7 @@ "git": { "name": "ffmpeg", "repositoryUrl": "https://chromium.googlesource.com/chromium/third_party/ffmpeg", - "commitHash": "0ba37733400593b162e5ae9ff26b384cff49c250" + "commitHash": "e1ca3f06adec15150a171bc38f550058b4bbb23b" } }, "isOnlyProductionDependency": true, @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "2e414d5d1082233c3516fca923fe351d5186c80e" + "commitHash": "8a01b3dcb7d08a48bfd3e6bf85ef49faa1454839" } }, "isOnlyProductionDependency": true, - "version": "18.17.1" + "version": "18.18.2" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "077c4addd5faa3ad1d1c9e598284368394a97fdd" + "commitHash": "2977fc4025fbc4c02ae9e87e480a94062b2ca4da" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "27.3.2" + "version": "28.2.6" }, { "component": { diff --git a/code/cli/Cargo.lock b/code/cli/Cargo.lock index 553df3f8f53..4be3e46eb7f 100644 --- a/code/cli/Cargo.lock +++ b/code/cli/Cargo.lock @@ -1236,9 +1236,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libz-sys" @@ -1330,14 +1330,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", + "windows-sys 0.48.0", ] [[package]] @@ -2526,7 +2525,7 @@ dependencies = [ [[package]] name = "tunnels" version = "0.1.0" -source = "git+https://github.com/microsoft/dev-tunnels?rev=4de1ff7979b5758c69218a3f45f6d9784b165072#4de1ff7979b5758c69218a3f45f6d9784b165072" +source = "git+https://github.com/microsoft/dev-tunnels?rev=8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30#8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30" dependencies = [ "async-trait", "chrono", diff --git a/code/cli/Cargo.toml b/code/cli/Cargo.toml index f51f31e9fb5..db058cd9f7c 100644 --- a/code/cli/Cargo.toml +++ b/code/cli/Cargo.toml @@ -34,7 +34,7 @@ serde_bytes = "0.11.9" chrono = { version = "0.4.26", features = ["serde", "std", "clock"], default-features = false } gethostname = "0.4.3" libc = "0.2.144" -tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "4de1ff7979b5758c69218a3f45f6d9784b165072", default-features = false, features = ["connections"] } +tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30", 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 = { version = "0.14.26", features = ["server", "http1", "runtime"] } diff --git a/code/cli/src/auth.rs b/code/cli/src/auth.rs index ee7117330be..2ee4f73c919 100644 --- a/code/cli/src/auth.rs +++ b/code/cli/src/auth.rs @@ -404,7 +404,10 @@ impl Auth { let mut keyring_storage = KeyringStorage::default(); #[cfg(target_os = "linux")] let mut keyring_storage = ThreadKeyringStorage::default(); - let mut file_storage = FileStorage(PersistedState::new(self.file_storage_path.clone())); + let mut file_storage = FileStorage(PersistedState::new_with_mode( + self.file_storage_path.clone(), + 0o600, + )); let native_storage_result = if std::env::var("VSCODE_CLI_USE_FILE_KEYCHAIN").is_ok() || self.file_storage_path.exists() diff --git a/code/cli/src/bin/code/legacy_args.rs b/code/cli/src/bin/code/legacy_args.rs index 3f134443641..0bd92c92fd3 100644 --- a/code/cli/src/bin/code/legacy_args.rs +++ b/code/cli/src/bin/code/legacy_args.rs @@ -42,6 +42,9 @@ pub fn try_parse_legacy( } } } else if let Ok(value) = arg.to_value() { + if value == "tunnel" { + return None; + } if let Some(last_arg) = &last_arg { args.get_mut(last_arg) .expect("expected to have last arg") diff --git a/code/cli/src/commands/args.rs b/code/cli/src/commands/args.rs index 229d54ad061..11f6e93c6e3 100644 --- a/code/cli/src/commands/args.rs +++ b/code/cli/src/commands/args.rs @@ -201,12 +201,18 @@ pub struct ServeWebArgs { /// A secret that must be included with all requests. #[clap(long)] pub connection_token: Option, + /// A file containing a secret that must be included with all requests. + #[clap(long)] + pub connection_token_file: 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 path under which the web UI and the code server is provided. + #[clap(long)] + pub server_base_path: Option, /// Specifies the directory that server data is kept in. #[clap(long)] pub server_data_dir: Option, @@ -652,6 +658,17 @@ pub struct TunnelServeArgs { /// 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, + + /// Requests that extensions be preloaded and installed on connecting servers. + #[clap(long)] + pub install_extension: Vec, +} + +impl TunnelServeArgs { + pub fn apply_to_server_args(&self, csa: &mut CodeServerArgs) { + csa.install_extensions + .extend_from_slice(&self.install_extension); + } } #[derive(Args, Debug, Clone)] diff --git a/code/cli/src/commands/serve_web.rs b/code/cli/src/commands/serve_web.rs index 959763a431d..fba92723426 100644 --- a/code/cli/src/commands/serve_web.rs +++ b/code/cli/src/commands/serve_web.rs @@ -5,8 +5,10 @@ use std::collections::HashMap; use std::convert::Infallible; +use std::fs; +use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -76,15 +78,14 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result Result>, } + +fn mint_connection_token(path: &Path, prefer_token: Option) -> std::io::Result { + #[cfg(not(windows))] + use std::os::unix::fs::OpenOptionsExt; + + let mut f = fs::OpenOptions::new(); + f.create(true); + f.write(true); + f.read(true); + #[cfg(not(windows))] + f.mode(0o600); + let mut f = f.open(path)?; + + if prefer_token.is_none() { + let mut t = String::new(); + f.read_to_string(&mut t)?; + let t = t.trim(); + if !t.is_empty() { + return Ok(t.to_string()); + } + } + + f.set_len(0)?; + let prefer_token = prefer_token.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + f.write_all(prefer_token.as_bytes())?; + Ok(prefer_token) +} diff --git a/code/cli/src/commands/tunnels.rs b/code/cli/src/commands/tunnels.rs index 02a697c1793..59c5794bd93 100644 --- a/code/cli/src/commands/tunnels.rs +++ b/code/cli/src/commands/tunnels.rs @@ -50,7 +50,11 @@ use crate::{ AuthRequired, Next, ServeStreamParams, ServiceContainer, ServiceManager, }, util::{ - app_lock::AppMutex, command::new_std_command, errors::{wrap, AnyError, CodeError}, machine::canonical_exe, prereqs::PreReqChecker + app_lock::AppMutex, + command::new_std_command, + errors::{wrap, AnyError, CodeError}, + machine::canonical_exe, + prereqs::PreReqChecker, }, }; use crate::{ @@ -227,8 +231,7 @@ pub async fn service( // likewise for license consent legal::require_consent(&ctx.paths, args.accept_server_license_terms)?; - let current_exe = - canonical_exe().map_err(|e| wrap(e, "could not get current exe"))?; + let current_exe = canonical_exe().map_err(|e| wrap(e, "could not get current exe"))?; manager .register( @@ -404,7 +407,8 @@ pub async fn serve(ctx: CommandContext, gateway_args: TunnelServeArgs) -> Result legal::require_consent(&paths, gateway_args.accept_server_license_terms)?; - let csa = (&args).into(); + let mut csa = (&args).into(); + gateway_args.apply_to_server_args(&mut csa); let result = serve_with_csa(paths, log, gateway_args, csa, TUNNEL_CLI_LOCK_NAME).await; drop(no_sleep); diff --git a/code/cli/src/state.rs b/code/cli/src/state.rs index 8815e2df40c..534c1556763 100644 --- a/code/cli/src/state.rs +++ b/code/cli/src/state.rs @@ -6,7 +6,8 @@ extern crate dirs; use std::{ - fs::{create_dir_all, read_to_string, remove_dir_all, write}, + fs::{self, create_dir_all, read_to_string, remove_dir_all}, + io::Write, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; @@ -34,6 +35,8 @@ where { path: PathBuf, state: Option, + #[allow(dead_code)] + mode: u32, } impl PersistedStateContainer @@ -58,13 +61,28 @@ where fn save(&mut self, state: T) -> Result<(), WrappedError> { let s = serde_json::to_string(&state).unwrap(); self.state = Some(state); - write(&self.path, s).map_err(|e| { + self.write_state(s).map_err(|e| { wrap( e, format!("error saving launcher state into {}", self.path.display()), ) }) } + + fn write_state(&mut self, s: String) -> std::io::Result<()> { + #[cfg(not(windows))] + use std::os::unix::fs::OpenOptionsExt; + + let mut f = fs::OpenOptions::new(); + f.create(true); + f.write(true); + f.truncate(true); + #[cfg(not(windows))] + f.mode(self.mode); + + let mut f = f.open(&self.path)?; + f.write_all(s.as_bytes()) + } } /// Container that holds some state value that is persisted to disk. @@ -82,8 +100,17 @@ where { /// Creates a new state container that persists to the given path. pub fn new(path: PathBuf) -> PersistedState { + Self::new_with_mode(path, 0o644) + } + + /// Creates a new state container that persists to the given path. + pub fn new_with_mode(path: PathBuf, mode: u32) -> PersistedState { PersistedState { - container: Arc::new(Mutex::new(PersistedStateContainer { path, state: None })), + container: Arc::new(Mutex::new(PersistedStateContainer { + path, + state: None, + mode, + })), } } @@ -217,5 +244,4 @@ impl LauncherPaths { pub fn web_server_storage(&self) -> PathBuf { self.root.join("serve-web") } - } diff --git a/code/cli/src/tunnels/code_server.rs b/code/cli/src/tunnels/code_server.rs index bb854001d54..592ad121291 100644 --- a/code/cli/src/tunnels/code_server.rs +++ b/code/cli/src/tunnels/code_server.rs @@ -15,7 +15,8 @@ use crate::update_service::{ unzip_downloaded_release, Platform, Release, TargetKind, UpdateService, }; use crate::util::command::{ - capture_command, capture_command_and_check_status, kill_tree, new_script_command, + capture_command, capture_command_and_check_status, check_output_status, kill_tree, + new_script_command, }; use crate::util::errors::{wrap, AnyError, CodeError, ExtensionInstallFailed, WrappedError}; use crate::util::http::{self, BoxedHttp}; @@ -488,6 +489,28 @@ impl<'a> ServerBuilder<'a> { }) } + /// Runs the command that just installs extensions and exits. + pub async fn install_extensions(&self) -> Result<(), AnyError> { + // cmd already has --install-extensions from base + let mut cmd = self.get_base_command(); + let cmd_str = || { + self.server_params + .code_server_args + .command_arguments() + .join(" ") + }; + + let r = cmd.output().await.map_err(|e| CodeError::CommandFailed { + command: cmd_str(), + code: -1, + output: e.to_string(), + })?; + + check_output_status(r, cmd_str)?; + + Ok(()) + } + pub async fn listen_on_default_socket(&self) -> Result { let requested_file = get_socket_name(); self.listen_on_socket(&requested_file).await diff --git a/code/cli/src/tunnels/control_server.rs b/code/cli/src/tunnels/control_server.rs index 5f564494b98..9aae5ef3f07 100644 --- a/code/cli/src/tunnels/control_server.rs +++ b/code/cli/src/tunnels/control_server.rs @@ -6,6 +6,7 @@ use crate::async_pipe::get_socket_rw_stream; use crate::constants::{CONTROL_PORT, PRODUCT_NAME_LONG}; use crate::log; use crate::msgpack_rpc::{new_msgpack_rpc, start_msgpack_rpc, MsgPackCodec, MsgPackSerializer}; +use crate::options::Quality; use crate::rpc::{MaybeSync, RpcBuilder, RpcCaller, RpcDispatcher}; use crate::self_update::SelfUpdate; use crate::state::LauncherPaths; @@ -144,6 +145,31 @@ pub struct ServerTermination { pub tunnel: ActiveTunnel, } +async fn preload_extensions( + log: &log::Logger, + platform: Platform, + mut args: CodeServerArgs, + launcher_paths: LauncherPaths, +) -> Result<(), AnyError> { + args.start_server = false; + + let params_raw = ServerParamsRaw { + commit_id: None, + quality: Quality::Stable, + code_server_args: args.clone(), + headless: true, + platform, + }; + + // cannot use delegated HTTP here since there's no remote connection yet + let http = Arc::new(ReqwestSimpleHttp::new()); + let resolved = params_raw.resolve(log, http.clone()).await?; + let sb = ServerBuilder::new(log, &resolved, &launcher_paths, http.clone()); + + sb.setup().await?; + sb.install_extensions().await +} + // Runs the launcher server. Exits on a ctrl+c or when requested by a user. // Note that client connections may not be closed when this returns; use // `close_all_clients()` on the ServerTermination to make this happen. @@ -160,6 +186,26 @@ pub async fn serve( let (tx, mut rx) = mpsc::channel::(4); let (exit_barrier, signal_exit) = new_barrier(); + if !code_server_args.install_extensions.is_empty() { + info!( + log, + "Preloading extensions using stable server: {:?}", code_server_args.install_extensions + ); + let log = log.clone(); + let code_server_args = code_server_args.clone(); + let launcher_paths = launcher_paths.clone(); + // This is run async to the primary tunnel setup to be speedy. + tokio::spawn(async move { + if let Err(e) = + preload_extensions(&log, platform, code_server_args, launcher_paths).await + { + warning!(log, "Failed to preload extensions: {:?}", e); + } else { + info!(log, "Extension install complete"); + } + }); + } + loop { tokio::select! { Ok(reason) = shutdown_rx.wait() => { diff --git a/code/cli/src/tunnels/dev_tunnels.rs b/code/cli/src/tunnels/dev_tunnels.rs index 94396e89977..3bf8a331e74 100644 --- a/code/cli/src/tunnels/dev_tunnels.rs +++ b/code/cli/src/tunnels/dev_tunnels.rs @@ -562,6 +562,10 @@ impl DevTunnels { let tunnel = match self.get_existing_tunnel_with_name(name).await? { Some(e) => { + if tunnel_has_host_connection(&e) { + return Err(CodeError::TunnelActiveAndInUse(name.to_string()).into()); + } + let loc = TunnelLocator::try_from(&e).unwrap(); info!(self.log, "Adopting existing tunnel (ID={:?})", loc); spanf!( @@ -687,13 +691,7 @@ impl DevTunnels { let recyclable = existing_tunnels .iter() - .filter(|t| { - t.status - .as_ref() - .and_then(|s| s.host_connection_count.as_ref()) - .map(|c| c.get_count()) - .unwrap_or(0) == 0 - }) + .filter(|t| !tunnel_has_host_connection(t)) .choose(&mut rand::thread_rng()); match recyclable { @@ -764,12 +762,9 @@ impl DevTunnels { ) -> Result { let existing_tunnels = self.list_tunnels_with_tag(&[self.tag]).await?; let is_name_free = |n: &str| { - !existing_tunnels.iter().any(|v| { - v.status - .as_ref() - .and_then(|s| s.host_connection_count.as_ref().map(|c| c.get_count())) - .unwrap_or(0) > 0 && v.labels.iter().any(|t| t == n) - }) + !existing_tunnels + .iter() + .any(|v| tunnel_has_host_connection(v) && v.labels.iter().any(|t| t == n)) }; if let Some(machine_name) = preferred_name { @@ -1235,6 +1230,14 @@ fn privacy_to_tunnel_acl(privacy: PortPrivacy) -> TunnelAccessControl { } } +fn tunnel_has_host_connection(tunnel: &Tunnel) -> bool { + tunnel + .status + .as_ref() + .and_then(|s| s.host_connection_count.as_ref().map(|c| c.get_count() > 0)) + .unwrap_or_default() +} + #[cfg(test)] mod test { use super::*; diff --git a/code/cli/src/tunnels/socket_signal.rs b/code/cli/src/tunnels/socket_signal.rs index 9036c6ae3f9..2227f323852 100644 --- a/code/cli/src/tunnels/socket_signal.rs +++ b/code/cli/src/tunnels/socket_signal.rs @@ -94,41 +94,42 @@ impl ServerMessageSink { async fn server_message_or_closed( &mut self, - body: Option<&[u8]>, + body_or_end: Option<&[u8]>, ) -> Result<(), mpsc::error::SendError> { let i = self.id; let mut tx = self.tx.take().unwrap(); - let msg = body - .map(|b| self.get_server_msg_content(b)) - .map(|body| RefServerMessageParams { i, body }); - - let r = match &mut tx { - ServerMessageDestination::Channel(tx) => { - tx.send(SocketSignal::from_message(&ToClientRequest { - id: None, - params: match msg { - Some(msg) => ClientRequestMethod::servermsg(msg), - None => ClientRequestMethod::serverclose(ServerClosedParams { i }), - }, - })) - .await - } - ServerMessageDestination::Rpc(caller) => { - match msg { - Some(msg) => caller.notify("servermsg", msg), - None => caller.notify("serverclose", ServerClosedParams { i }), - }; - Ok(()) - } - }; + if let Some(b) = body_or_end { + let body = self.get_server_msg_content(b, false); + let r = + send_data_or_close_if_none(i, &mut tx, Some(RefServerMessageParams { i, body })) + .await; + self.tx = Some(tx); + return r; + } + + let tail = self.get_server_msg_content(&[], true); + if !tail.is_empty() { + let _ = send_data_or_close_if_none( + i, + &mut tx, + Some(RefServerMessageParams { i, body: tail }), + ) + .await; + } + + let r = send_data_or_close_if_none(i, &mut tx, None).await; self.tx = Some(tx); r } - pub(crate) fn get_server_msg_content<'a: 'b, 'b>(&'a mut self, body: &'b [u8]) -> &'b [u8] { + pub(crate) fn get_server_msg_content<'a: 'b, 'b>( + &'a mut self, + body: &'b [u8], + finish: bool, + ) -> &'b [u8] { if let Some(flate) = &mut self.flate { - if let Ok(compressed) = flate.process(body) { + if let Ok(compressed) = flate.process(body, finish) { return compressed; } } @@ -137,6 +138,32 @@ impl ServerMessageSink { } } +async fn send_data_or_close_if_none( + i: u16, + tx: &mut ServerMessageDestination, + msg: Option>, +) -> Result<(), mpsc::error::SendError> { + match tx { + ServerMessageDestination::Channel(tx) => { + tx.send(SocketSignal::from_message(&ToClientRequest { + id: None, + params: match msg { + Some(msg) => ClientRequestMethod::servermsg(msg), + None => ClientRequestMethod::serverclose(ServerClosedParams { i }), + }, + })) + .await + } + ServerMessageDestination::Rpc(caller) => { + match msg { + Some(msg) => caller.notify("servermsg", msg), + None => caller.notify("serverclose", ServerClosedParams { i }), + }; + Ok(()) + } + } +} + impl Drop for ServerMessageSink { fn drop(&mut self) { self.multiplexer.remove(self.id); @@ -162,7 +189,8 @@ impl ClientMessageDecoder { pub fn decode<'a: 'b, 'b>(&'a mut self, message: &'b [u8]) -> std::io::Result<&'b [u8]> { match &mut self.dec { - Some(d) => d.process(message), + // todo@connor4312 do we ever need to actually 'finish' the client message stream? + Some(d) => d.process(message, false), None => Ok(message), } } @@ -175,6 +203,7 @@ trait FlateAlgorithm { &mut self, contents: &[u8], output: &mut [u8], + finish: bool, ) -> Result; } @@ -193,9 +222,15 @@ impl FlateAlgorithm for DecompressFlateAlgorithm { &mut self, contents: &[u8], output: &mut [u8], + finish: bool, ) -> Result { + let mode = match finish { + true => flate2::FlushDecompress::Finish, + false => flate2::FlushDecompress::None, + }; + self.0 - .decompress(contents, output, flate2::FlushDecompress::None) + .decompress(contents, output, mode) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e)) } } @@ -215,9 +250,15 @@ impl FlateAlgorithm for CompressFlateAlgorithm { &mut self, contents: &[u8], output: &mut [u8], + finish: bool, ) -> Result { + let mode = match finish { + true => flate2::FlushCompress::Finish, + false => flate2::FlushCompress::Sync, + }; + self.0 - .compress(contents, output, flate2::FlushCompress::Sync) + .compress(contents, output, mode) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e)) } } @@ -241,23 +282,25 @@ where } } - pub fn process(&mut self, contents: &[u8]) -> std::io::Result<&[u8]> { + pub fn process(&mut self, contents: &[u8], finish: bool) -> std::io::Result<&[u8]> { let mut out_offset = 0; let mut in_offset = 0; loop { let in_before = self.flate.total_in(); let out_before = self.flate.total_out(); - match self - .flate - .process(&contents[in_offset..], &mut self.output[out_offset..]) - { + match self.flate.process( + &contents[in_offset..], + &mut self.output[out_offset..], + finish, + ) { Ok(flate2::Status::Ok | flate2::Status::BufError) => { let processed_len = in_offset + (self.flate.total_in() - in_before) as usize; let output_len = out_offset + (self.flate.total_out() - out_before) as usize; - if processed_len < contents.len() { + if processed_len < contents.len() || output_len == self.output.len() { // If we filled the output buffer but there's more data to compress, - // extend the output buffer and keep compressing. + // or the output got filled after processing all input, extend + // the output buffer and keep compressing. out_offset = output_len; in_offset = processed_len; if output_len == self.output.len() { @@ -298,7 +341,7 @@ mod tests { // 3000 and 30000 test resizing the buffer for msg_len in [3, 30, 300, 3000, 30000] { let vals = (0..msg_len).map(|v| v as u8).collect::>(); - let compressed = sink.get_server_msg_content(&vals); + let compressed = sink.get_server_msg_content(&vals, false); assert_ne!(compressed, vals); let decompressed = decompress.decode(compressed).unwrap(); assert_eq!(decompressed.len(), vals.len()); diff --git a/code/cli/src/util/errors.rs b/code/cli/src/util/errors.rs index 03280d12f0a..0659a0d1781 100644 --- a/code/cli/src/util/errors.rs +++ b/code/cli/src/util/errors.rs @@ -512,6 +512,10 @@ pub enum CodeError { // todo: can be specialized when update service is moved to CodeErrors #[error("Could not check for update: {0}")] UpdateCheckFailed(String), + #[error("Could not write connection token file: {0}")] + CouldNotCreateConnectionTokenFile(std::io::Error), + #[error("A tunnel with the name {0} exists and is in-use. Please pick a different name or stop the existing tunnel.")] + TunnelActiveAndInUse(String), } makeAnyError!( diff --git a/code/extensions/dart/cgmanifest.json b/code/extensions/dart/cgmanifest.json index 0086a5158e5..9c90588adf1 100644 --- a/code/extensions/dart/cgmanifest.json +++ b/code/extensions/dart/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dart-lang/dart-syntax-highlight", "repositoryUrl": "https://github.com/dart-lang/dart-syntax-highlight", - "commitHash": "0a6648177bdbb91a4e1a38c16e57ede0ccba4f18" + "commitHash": "272e2f89f85073c04b7e15b582257f76d2489970" } }, "licenseDetail": [ diff --git a/code/extensions/dart/syntaxes/dart.tmLanguage.json b/code/extensions/dart/syntaxes/dart.tmLanguage.json index ae4db9698e9..cc9dee8d275 100644 --- a/code/extensions/dart/syntaxes/dart.tmLanguage.json +++ b/code/extensions/dart/syntaxes/dart.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dart-lang/dart-syntax-highlight/commit/0a6648177bdbb91a4e1a38c16e57ede0ccba4f18", + "version": "https://github.com/dart-lang/dart-syntax-highlight/commit/272e2f89f85073c04b7e15b582257f76d2489970", "name": "Dart", "scopeName": "source.dart", "patterns": [ @@ -308,7 +308,7 @@ }, { "name": "keyword.control.dart", - "match": "(? { + this.diffStageHunkOrSelection(changes); + } + + @command('git.diff.stageSelection') + async diffStageSelection(changes: DiffEditorSelectionHunkToolbarContext): Promise { + this.diffStageHunkOrSelection(changes); + } + + async diffStageHunkOrSelection(changes: DiffEditorSelectionHunkToolbarContext): Promise { + const textEditor = window.activeTextEditor; + if (!textEditor) { + return; + } + const modifiedDocument = textEditor.document; + const modifiedUri = modifiedDocument.uri; + if (modifiedUri.scheme !== 'file') { + return; + } + const result = changes.originalWithModifiedChanges; + await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result)); + } + @command('git.stageSelectedRanges', { diff: true }) async stageSelectedChanges(changes: LineChange[]): Promise { const textEditor = window.activeTextEditor; diff --git a/code/extensions/git/src/decorationProvider.ts b/code/extensions/git/src/decorationProvider.ts index 5167b1eb95e..3aae16f6baf 100644 --- a/code/extensions/git/src/decorationProvider.ts +++ b/code/extensions/git/src/decorationProvider.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode'; +import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n } from 'vscode'; import * as path from 'path'; import { Repository, GitResourceGroup } from './repository'; import { Model } from './model'; import { debounce } from './decorators'; import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable } from './util'; -import { GitErrorCodes, Status } from './api/git'; +import { Change, GitErrorCodes, Status } from './api/git'; class GitIgnoreDecorationProvider implements FileDecorationProvider { @@ -153,100 +153,95 @@ class GitDecorationProvider implements FileDecorationProvider { } } -// class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { - -// private readonly _onDidChangeDecorations = new EventEmitter(); -// readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; - -// private decorations = new Map(); -// private readonly disposables: Disposable[] = []; - -// constructor(private readonly repository: Repository) { -// this.disposables.push(window.registerFileDecorationProvider(this)); -// repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); -// } - -// private async onDidChangeCurrentHistoryItemGroup(): Promise { -// const newDecorations = new Map(); -// await this.collectIncomingChangesFileDecorations(newDecorations); -// const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); - -// this.decorations = newDecorations; -// this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); -// } - -// private async collectIncomingChangesFileDecorations(bucket: Map): Promise { -// for (const change of await this.getIncomingChanges()) { -// switch (change.status) { -// case Status.INDEX_ADDED: -// bucket.set(change.uri.toString(), { -// badge: '↓A', -// color: new ThemeColor('gitDecoration.incomingAddedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (added)'), -// }); -// break; -// case Status.DELETED: -// bucket.set(change.uri.toString(), { -// badge: '↓D', -// color: new ThemeColor('gitDecoration.incomingDeletedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (deleted)'), -// }); -// break; -// case Status.INDEX_RENAMED: -// bucket.set(change.originalUri.toString(), { -// badge: '↓R', -// color: new ThemeColor('gitDecoration.incomingRenamedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (renamed)'), -// }); -// break; -// case Status.MODIFIED: -// bucket.set(change.uri.toString(), { -// badge: '↓M', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (modified)'), -// }); -// break; -// default: { -// bucket.set(change.uri.toString(), { -// badge: '↓~', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes'), -// }); -// break; -// } -// } -// } -// } - -// private async getIncomingChanges(): Promise { -// try { -// const historyProvider = this.repository.historyProvider; -// const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; - -// if (!currentHistoryItemGroup?.base) { -// return []; -// } - -// const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); -// if (!ancestor) { -// return []; -// } - -// const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); -// return changes; -// } catch (err) { -// return []; -// } -// } - -// provideFileDecoration(uri: Uri): FileDecoration | undefined { -// return this.decorations.get(uri.toString()); -// } - -// dispose(): void { -// dispose(this.disposables); -// } -// } +class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { + + private readonly _onDidChangeDecorations = new EventEmitter(); + readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; + + private decorations = new Map(); + private readonly disposables: Disposable[] = []; + + constructor(private readonly repository: Repository) { + this.disposables.push(window.registerFileDecorationProvider(this)); + repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); + } + + private async onDidChangeCurrentHistoryItemGroup(): Promise { + const newDecorations = new Map(); + await this.collectIncomingChangesFileDecorations(newDecorations); + const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); + + this.decorations = newDecorations; + this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); + } + + private async collectIncomingChangesFileDecorations(bucket: Map): Promise { + for (const change of await this.getIncomingChanges()) { + switch (change.status) { + case Status.INDEX_ADDED: + bucket.set(change.uri.toString(), { + badge: '↓A', + tooltip: l10n.t('Incoming Changes (added)'), + }); + break; + case Status.DELETED: + bucket.set(change.uri.toString(), { + badge: '↓D', + tooltip: l10n.t('Incoming Changes (deleted)'), + }); + break; + case Status.INDEX_RENAMED: + bucket.set(change.originalUri.toString(), { + badge: '↓R', + tooltip: l10n.t('Incoming Changes (renamed)'), + }); + break; + case Status.MODIFIED: + bucket.set(change.uri.toString(), { + badge: '↓M', + tooltip: l10n.t('Incoming Changes (modified)'), + }); + break; + default: { + bucket.set(change.uri.toString(), { + badge: '↓~', + tooltip: l10n.t('Incoming Changes'), + }); + break; + } + } + } + } + + private async getIncomingChanges(): Promise { + try { + const historyProvider = this.repository.historyProvider; + const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; + + if (!currentHistoryItemGroup?.base) { + return []; + } + + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); + if (!ancestor) { + return []; + } + + const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); + return changes; + } catch (err) { + return []; + } + } + + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.decorations.get(uri.toString()); + } + + dispose(): void { + dispose(this.disposables); + } +} export class GitDecorations { @@ -287,7 +282,7 @@ export class GitDecorations { private onDidOpenRepository(repository: Repository): void { const providers = combinedDisposable([ new GitDecorationProvider(repository), - // new GitIncomingChangesFileDecorationProvider(repository) + new GitIncomingChangesFileDecorationProvider(repository) ]); this.providers.set(repository, providers); diff --git a/code/extensions/git/src/git.ts b/code/extensions/git/src/git.ts index 738295bdec1..710d7a4d110 100644 --- a/code/extensions/git/src/git.ts +++ b/code/extensions/git/src/git.ts @@ -1161,6 +1161,10 @@ export class Repository { args.push(`-n${options?.maxEntries ?? 32}`); } + if (options?.author) { + args.push(`--author="${options.author}"`); + } + if (options?.path) { args.push('--', options.path); } diff --git a/code/extensions/git/src/staging.ts b/code/extensions/git/src/staging.ts index 2813bfb1ee9..bbc7055cfd4 100644 --- a/code/extensions/git/src/staging.ts +++ b/code/extensions/git/src/staging.ts @@ -142,3 +142,11 @@ export function invertLineChange(diff: LineChange): LineChange { originalEndLineNumber: diff.modifiedEndLineNumber }; } + +export interface DiffEditorSelectionHunkToolbarContext { + mapping: unknown; + /** + * The original text with the selected modified changes applied. + */ + originalWithModifiedChanges: string; +} diff --git a/code/extensions/github-authentication/src/common/errors.ts b/code/extensions/github-authentication/src/common/errors.ts index 3ba3dfc006a..f60b7233499 100644 --- a/code/extensions/github-authentication/src/common/errors.ts +++ b/code/extensions/github-authentication/src/common/errors.ts @@ -8,3 +8,7 @@ export const TIMED_OUT_ERROR = 'Timed out'; // These error messages are internal and should not be shown to the user in any way. export const USER_CANCELLATION_ERROR = 'User Cancelled'; export const NETWORK_ERROR = 'network error'; + +// This is the error message that we throw if the login was cancelled for any reason. Extensions +// calling `getSession` can handle this error to know that the user cancelled the login. +export const CANCELLATION_ERROR = 'Cancelled'; diff --git a/code/extensions/github-authentication/src/flows.ts b/code/extensions/github-authentication/src/flows.ts index 3641ffb3a36..7498a2b2202 100644 --- a/code/extensions/github-authentication/src/flows.ts +++ b/code/extensions/github-authentication/src/flows.ts @@ -68,6 +68,7 @@ interface IFlowTriggerOptions { callbackUri: Uri; uriHandler: UriEventHandler; enterpriseUri?: Uri; + existingLogin?: string; } interface IFlow { @@ -149,7 +150,8 @@ const allFlows: IFlow[] = [ nonce, callbackUri, uriHandler, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying without local server... (${scopes})`); return await window.withProgress({ @@ -169,6 +171,9 @@ const allFlows: IFlow[] = [ ['scope', scopes], ['state', encodeURIComponent(callbackUri.toString(true))] ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } // The extra toString, parse is apparently needed for env.openExternal // to open the correct URL. @@ -215,7 +220,8 @@ const allFlows: IFlow[] = [ baseUri, redirectUri, logger, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying with local server... (${scopes})`); return await window.withProgress({ @@ -232,6 +238,9 @@ const allFlows: IFlow[] = [ ['redirect_uri', redirectUri.toString(true)], ['scope', scopes], ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } const loginUrl = baseUri.with({ path: '/login/oauth/authorize', diff --git a/code/extensions/github-authentication/src/github.ts b/code/extensions/github-authentication/src/github.ts index 71aa17bd5cc..3d73bfb7656 100644 --- a/code/extensions/github-authentication/src/github.ts +++ b/code/extensions/github-authentication/src/github.ts @@ -11,7 +11,7 @@ import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './common/experimentationService'; import { Log } from './common/logger'; import { crypto } from './node/crypto'; -import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -296,13 +296,44 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid scopes: JSON.stringify(scopes), }); + const sessions = await this._sessionsPromise; const scopeString = sortedScopes.join(' '); - const token = await this._githubServer.login(scopeString); + const existingLogin = sessions[0]?.account.label; + const token = await this._githubServer.login(scopeString, existingLogin); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - const sessions = await this._sessionsPromise; + if (sessions.some(s => s.account.id !== session.account.id)) { + const otherAccountsIndexes = new Array(); + const otherAccountsLabels = new Set(); + for (let i = 0; i < sessions.length; i++) { + if (sessions[i].account.id !== session.account.id) { + otherAccountsIndexes.push(i); + otherAccountsLabels.add(sessions[i].account.label); + } + } + const proceed = vscode.l10n.t("Continue"); + const labelstr = [...otherAccountsLabels].join(', '); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t({ + message: "You are logged into another account already ({0}).\n\nDo you want to log out of that account and log in to '{1}' instead?", + comment: ['{0} is a comma-separated list of account names. {1} is the account name to log into.'], + args: [labelstr, session.account.label] + }), + { modal: true }, + proceed + ); + if (result !== proceed) { + throw new Error(CANCELLATION_ERROR); + } + + // Remove other accounts + for (const i of otherAccountsIndexes) { + sessions.splice(i, 1); + } + } + const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); if (sessionIndex > -1) { sessions.splice(sessionIndex, 1, session); diff --git a/code/extensions/github-authentication/src/githubServer.ts b/code/extensions/github-authentication/src/githubServer.ts index 0729c4c5077..af2cf22724f 100644 --- a/code/extensions/github-authentication/src/githubServer.ts +++ b/code/extensions/github-authentication/src/githubServer.ts @@ -11,19 +11,15 @@ import { isSupportedClient, isSupportedTarget } from './common/env'; import { crypto } from './node/crypto'; import { fetching } from './node/fetch'; import { ExtensionHost, GitHubTarget, getFlows } from './flows'; -import { NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; import { Config } from './config'; import { base64Encode } from './node/buffer'; -// This is the error message that we throw if the login was cancelled for any reason. Extensions -// calling `getSession` can handle this error to know that the user cancelled the login. -const CANCELLATION_ERROR = 'Cancelled'; - const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect'; const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; export interface IGitHubServer { - login(scopes: string): Promise; + login(scopes: string, existingLogin?: string): Promise; logout(session: vscode.AuthenticationSession): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise; @@ -91,7 +87,7 @@ export class GitHubServer implements IGitHubServer { return this._isNoCorsEnvironment; } - public async login(scopes: string): Promise { + public async login(scopes: string, existingLogin?: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); // Used for showing a friendlier message to the user when the explicitly cancel a flow. @@ -143,6 +139,7 @@ export class GitHubServer implements IGitHubServer { uriHandler: this._uriHandler, enterpriseUri: this._ghesUri, redirectUri: vscode.Uri.parse(await this.getRedirectEndpoint()), + existingLogin }); } catch (e) { userCancelled = this.processLoginError(e); diff --git a/code/extensions/go/cgmanifest.json b/code/extensions/go/cgmanifest.json index 2b837e80a2f..7b7bc3d51f9 100644 --- a/code/extensions/go/cgmanifest.json +++ b/code/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "de0edabe11035e7035155c68eddc5817d5ec4af9" + "commitHash": "f53c71e58787fb719399b7c38a08bceaa0c0e2d9" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.5.6" + "version": "0.6.1" } ], "version": 1 diff --git a/code/extensions/go/syntaxes/go.tmLanguage.json b/code/extensions/go/syntaxes/go.tmLanguage.json index 3641d3edf99..efd69afbcd2 100644 --- a/code/extensions/go/syntaxes/go.tmLanguage.json +++ b/code/extensions/go/syntaxes/go.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/worlpaker/go-syntax/commit/de0edabe11035e7035155c68eddc5817d5ec4af9", + "version": "https://github.com/worlpaker/go-syntax/commit/f53c71e58787fb719399b7c38a08bceaa0c0e2d9", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -499,7 +499,7 @@ "comment": "Note that the order here is very important!", "patterns": [ { - "match": "((?:\\*|&)+)(?:(?!\\d)(?=(?:[\\w\\[\\]])|(?:\\<\\-)))", + "match": "((?:\\*|\\&)+)(?:(?!\\d)(?=(?:[\\w\\[\\]])|(?:\\<\\-)))", "name": "keyword.operator.address.go" }, { @@ -1185,12 +1185,7 @@ "name": "entity.name.function.go" } ] - }, - "patterns": [ - { - "include": "#type-declarations" - } - ] + } }, "end": "(?:(?<=\\))\\s*((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?!(?:[\\[\\]\\*]+)?(?:\\bstruct\\b|\\binterface\\b))[\\w\\.\\-\\*\\[\\]]+)?\\s*(?=\\{))", "endCaptures": { @@ -1261,7 +1256,7 @@ }, { "comment": "single function as a type returned type(s) declaration", - "match": "(?:(?<=\\))\\s+((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?[\\w\\*\\.\\[\\]\\<\\>\\-]+(?:\\s*)(?:\\/(?:\\/|\\*).*)?)$)", + "match": "(?:(?<=\\))(?:\\s*)((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?[\\w\\*\\.\\[\\]\\<\\>\\-]+(?:\\s*)(?:\\/(?:\\/|\\*).*)?)$)", "captures": { "1": { "patterns": [ @@ -1272,7 +1267,7 @@ "include": "#parameter-variable-types" }, { - "match": "(?:\\w+)", + "match": "\\w+", "name": "entity.name.type.go" } ] @@ -1513,7 +1508,7 @@ }, "functions_inline": { "comment": "functions in-line with multi return types", - "match": "(?:(\\bfunc\\b)((?:\\((?:[^/]*)\\))(?:\\s+)(?:\\((?:[^/]*)\\)))(?:\\s+)(?=\\{))", + "match": "(?:(\\bfunc\\b)((?:\\((?:[^/]*?)\\))(?:\\s+)(?:\\((?:[^/]*?)\\)))(?:\\s+)(?=\\{))", "captures": { "1": { "name": "keyword.function.go" @@ -1571,7 +1566,7 @@ }, "support_functions": { "comment": "Support Functions", - "match": "(?:(?:((?<=\\.)\\w+)|(\\w+))(\\[(?:(?:[\\w\\.\\*\\[\\]\\{\\}\"\\']+)(?:(?:\\,\\s*(?:[\\w\\.\\*\\[\\]\\{\\}]+))*))?\\])?(?=\\())", + "match": "(?:(?:((?<=\\.)\\b\\w+)|(\\b\\w+))(\\[(?:(?:[\\w\\.\\*\\[\\]\\{\\}\"\\']+)(?:(?:\\,\\s*(?:[\\w\\.\\*\\[\\]\\{\\}]+))*))?\\])?(?=\\())", "captures": { "1": { "name": "entity.name.function.support.go" @@ -1867,11 +1862,18 @@ "patterns": [ { "comment": "Struct variable for struct in struct types", - "begin": "(?:\\s*)?([\\s\\,\\w]+)(?:\\s+)(?:(?:[\\[\\]\\*])+)?(\\bstruct\\b)\\s*(\\{)", + "begin": "(?:(\\w+(?:\\,\\s*\\w+)*)(?:\\s+)(?:(?:[\\[\\]\\*])+)?(\\bstruct\\b)(?:\\s*)(\\{))", "beginCaptures": { "1": { - "match": "(?:\\w+)", - "name": "variable.other.property.go" + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.other.property.go" + } + ] }, "2": { "name": "keyword.struct.go" @@ -1911,6 +1913,42 @@ } }, "patterns": [ + { + "include": "#support_functions" + }, + { + "include": "#type-declarations-without-brackets" + }, + { + "begin": "(?:([\\w\\.\\*]+)?(\\[))", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "(?:\\w+)", + "name": "entity.name.type.go" + } + ] + }, + "2": { + "name": "punctuation.definition.begin.bracket.square.go" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.end.bracket.square.go" + } + }, + "patterns": [ + { + "include": "#generic_param_types" + } + ] + }, { "begin": "\\(", "beginCaptures": { @@ -1927,18 +1965,12 @@ "patterns": [ { "include": "#function_param_types" - }, - { - "include": "$self" } ] }, { - "include": "#support_functions" - }, - { - "comment": "single declaration | with or declarations", - "match": "((?:\\s+\\|)?(?:[\\w\\.\\[\\]\\*]+)(?:\\s+\\|)?)", + "comment": "other types", + "match": "([\\w\\.]+)", "captures": { "1": { "patterns": [ @@ -1946,10 +1978,7 @@ "include": "#type-declarations" }, { - "include": "#generic_types" - }, - { - "match": "(?:\\w+)", + "match": "\\w+", "name": "entity.name.type.go" } ] @@ -2145,7 +2174,7 @@ }, "after_control_variables": { "comment": "After control variables, to not highlight as a struct/interface (before formatting with gofmt)", - "match": "(?:(?<=\\brange\\b|\\bswitch\\b|\\;|\\bif\\b|\\bfor\\b|\\<|\\>|\\<\\=|\\>\\=|\\=\\=|\\!\\=|\\w(?:\\+|/|\\-|\\*|\\%)|\\w(?:\\+|/|\\-|\\*|\\%)\\=|\\|\\||\\&\\&)(?:\\s*)([[:alnum:]\\-\\_\\!\\.\\[\\]\\<\\>\\=\\*/\\+\\%\\:]+)(?:\\s*)(?=\\{))", + "match": "(?:(?<=\\brange\\b|\\bswitch\\b|\\;|\\bif\\b|\\bfor\\b|\\<|\\>|\\<\\=|\\>\\=|\\=\\=|\\!\\=|\\w(?:\\+|/|\\-|\\*|\\%)|\\w(?:\\+|/|\\-|\\*|\\%)\\=|\\|\\||\\&\\&)(?:\\s*)((?![\\[\\]]+)[[:alnum:]\\-\\_\\!\\.\\[\\]\\<\\>\\=\\*/\\+\\%\\:]+)(?:\\s*)(?=\\{))", "captures": { "1": { "patterns": [ @@ -2234,7 +2263,7 @@ }, { "comment": "make keyword", - "match": "(?:(\\bmake\\b)(?:(\\()((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?:[\\w\\.\\*\\[\\]\\{\\}]+)?(?:\\[(?:[^\\]]+)?\\])?(?:[\\w\\.\\*\\[\\]\\{\\}]+)?)?((?:\\,\\s*(?:[\\w\\.\\(\\)]+)?)+)?(\\))))", + "match": "(?:(\\bmake\\b)(?:(\\()((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?:[\\w\\.\\*\\[\\]\\{\\}]+)?(?:\\[(?:[^\\]]+)?\\])?(?:[\\w\\.\\*\\[\\]\\{\\}]+)?)?((?:\\,\\s*(?:[\\w\\.\\(\\)/\\+\\-\\<\\>\\&\\|\\%\\*]+)?)+)?(\\))))", "captures": { "1": { "name": "entity.name.function.support.builtin.go" @@ -2291,6 +2320,7 @@ } }, "switch_types": { + "comment": "switch type assertions, only highlights types after case keyword", "begin": "(?<=\\bswitch\\b)(?:\\s*)(?:(\\w+\\s*\\:\\=)?\\s*([\\w\\.\\*\\(\\)\\[\\]]+))(\\.\\(\\btype\\b\\)\\s*)(\\{)", "beginCaptures": { "1": { @@ -2299,7 +2329,7 @@ "include": "#operators" }, { - "match": "(?:\\w+)", + "match": "\\w+", "name": "variable.other.assignment.go" } ] @@ -2313,7 +2343,7 @@ "include": "#type-declarations" }, { - "match": "(?:\\w+)", + "match": "\\w+", "name": "variable.other.go" } ] @@ -2344,9 +2374,7 @@ }, "patterns": [ { - "include": "#type-declarations" - }, - { + "comment": "types after case keyword with single line", "match": "(?:^\\s*(\\bcase\\b))(?:\\s+)([\\w\\.\\,\\*\\=\\<\\>\\!\\s]+)(:)(\\s*/(?:/|\\*)\\s*.*)?$", "captures": { "1": { @@ -2375,6 +2403,30 @@ } } }, + { + "comment": "types after case keyword with multi lines", + "begin": "\\bcase\\b", + "beginCaptures": { + "0": { + "name": "keyword.control.go" + } + }, + "end": "\\:", + "endCaptures": { + "0": { + "name": "punctuation.other.colon.go" + } + }, + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] + }, { "include": "$self" } @@ -2573,7 +2625,7 @@ } }, "switch_select_case_variables": { - "comment": "variables after case control keyword in switch/select expression", + "comment": "variables after case control keyword in switch/select expression, to not scope them as property variables", "match": "(?:(?:^\\s*(\\bcase\\b))(?:\\s+)([\\s\\S]+(?:\\:)\\s*(?:/(?:/|\\*).*)?)$)", "captures": { "1": { @@ -2587,6 +2639,9 @@ { "include": "#support_functions" }, + { + "include": "#variable_assignment" + }, { "match": "\\w+", "name": "variable.other.go" @@ -2710,7 +2765,7 @@ }, "double_parentheses_types": { "comment": "double parentheses types", - "match": "(?:(\\((?:[\\w\\.\\[\\]\\*\\&]+)\\))(?=\\())", + "match": "(?:(?= 4.5 have an id. - * Details of the spec can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html# - */ -export function ensureAllNewCellsHaveCellIds(context: ExtensionContext) { - workspace.onDidChangeNotebookDocument(onDidChangeNotebookCells, undefined, context.subscriptions); -} - -function onDidChangeNotebookCells(e: NotebookDocumentChangeEvent) { - const nbMetadata = getNotebookMetadata(e.notebook); - if (!isCellIdRequired(nbMetadata)) { - return; - } - e.contentChanges.forEach(change => { - change.addedCells.forEach(cell => { - const cellMetadata = getCellMetadata(cell); - if (cellMetadata?.id) { - return; - } - const id = generateCellId(e.notebook); - const edit = new WorkspaceEdit(); - // Don't edit the metadata directly, always get a clone (prevents accidental singletons and directly editing the objects). - const updatedMetadata: CellMetadata = { ...JSON.parse(JSON.stringify(cellMetadata || {})) }; - updatedMetadata.id = id; - edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, { ...(cell.metadata), custom: updatedMetadata })]); - workspace.applyEdit(edit); - }); - }); -} - -/** - * Cell ids are required in notebooks only in notebooks with nbformat >= 4.5 - */ -function isCellIdRequired(metadata: Pick, 'nbformat' | 'nbformat_minor'>) { - if ((metadata.nbformat || 0) >= 5) { - return true; - } - if ((metadata.nbformat || 0) === 4 && (metadata.nbformat_minor || 0) >= 5) { - return true; - } - return false; -} - -function generateCellId(notebook: NotebookDocument) { - while (true) { - // Details of the id can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#adding-an-id-field, - // & here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#updating-older-formats - const id = generateUuid().replace(/-/g, '').substring(0, 8); - let duplicate = false; - for (let index = 0; index < notebook.cellCount; index++) { - const cell = notebook.cellAt(index); - const existingId = getCellMetadata(cell)?.id; - if (!existingId) { - continue; - } - if (existingId === id) { - duplicate = true; - break; - } - } - if (!duplicate) { - return id; - } - } -} - - -/** - * Copied from src/vs/base/common/uuid.ts - */ -function generateUuid() { - // use `randomValues` if possible - function getRandomValues(bucket: Uint8Array): Uint8Array { - for (let i = 0; i < bucket.length; i++) { - bucket[i] = Math.floor(Math.random() * 256); - } - return bucket; - } - - // prep-work - const _data = new Uint8Array(16); - const _hex: string[] = []; - for (let i = 0; i < 256; i++) { - _hex.push(i.toString(16).padStart(2, '0')); - } - - // get data - getRandomValues(_data); - - // set version bits - _data[6] = (_data[6] & 0x0f) | 0x40; - _data[8] = (_data[8] & 0x3f) | 0x80; - - // print as string - let i = 0; - let result = ''; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - return result; -} diff --git a/code/extensions/ipynb/src/common.ts b/code/extensions/ipynb/src/common.ts index d5ff5f86069..b7b454ac03f 100644 --- a/code/extensions/ipynb/src/common.ts +++ b/code/extensions/ipynb/src/common.ts @@ -58,5 +58,5 @@ export interface CellMetadata { /** * Stores cell metadata. */ - metadata?: Partial; + metadata?: Partial & { vscode?: { languageId?: string } }; } diff --git a/code/extensions/ipynb/src/ipynbMain.ts b/code/extensions/ipynb/src/ipynbMain.ts index c256e3b4f65..55fd2b62c84 100644 --- a/code/extensions/ipynb/src/ipynbMain.ts +++ b/code/extensions/ipynb/src/ipynbMain.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { NotebookSerializer } from './notebookSerializer'; -import { ensureAllNewCellsHaveCellIds } from './cellIdService'; +import { activate as keepNotebookModelStoreInSync } from './notebookModelStoreSync'; import { notebookImagePasteSetup } from './notebookImagePaste'; import { AttachmentCleaner } from './notebookAttachmentCleaner'; @@ -30,7 +30,7 @@ type NotebookMetadata = { export function activate(context: vscode.ExtensionContext) { const serializer = new NotebookSerializer(context); - ensureAllNewCellsHaveCellIds(context); + keepNotebookModelStoreInSync(context); context.subscriptions.push(vscode.workspace.registerNotebookSerializer('jupyter-notebook', serializer, { transientOutputs: false, transientCellMetadata: { diff --git a/code/extensions/ipynb/src/notebookAttachmentCleaner.ts b/code/extensions/ipynb/src/notebookAttachmentCleaner.ts index cad19f07b29..32aae0c5d1e 100644 --- a/code/extensions/ipynb/src/notebookAttachmentCleaner.ts +++ b/code/extensions/ipynb/src/notebookAttachmentCleaner.ts @@ -81,34 +81,31 @@ export class AttachmentCleaner implements vscode.CodeActionProvider { this._disposables.push(vscode.workspace.onWillSaveNotebookDocument(e => { if (e.reason === vscode.TextDocumentSaveReason.Manual) { this._delayer.dispose(); - - e.waitUntil(new Promise((resolve) => { - if (e.notebook.getCells().length === 0) { - return; - } - - const notebookEdits: vscode.NotebookEdit[] = []; - for (const cell of e.notebook.getCells()) { - if (cell.kind !== vscode.NotebookCellKind.Markup) { - continue; - } - - const metadataEdit = this.cleanNotebookAttachments({ - notebook: e.notebook, - cell: cell, - document: cell.document - }); - - if (metadataEdit) { - notebookEdits.push(metadataEdit); - } + if (e.notebook.getCells().length === 0) { + return; + } + const notebookEdits: vscode.NotebookEdit[] = []; + for (const cell of e.notebook.getCells()) { + if (cell.kind !== vscode.NotebookCellKind.Markup) { + continue; } - const workspaceEdit = new vscode.WorkspaceEdit(); - workspaceEdit.set(e.notebook.uri, notebookEdits); + const metadataEdit = this.cleanNotebookAttachments({ + notebook: e.notebook, + cell: cell, + document: cell.document + }); - resolve(workspaceEdit); - })); + if (metadataEdit) { + notebookEdits.push(metadataEdit); + } + } + if (!notebookEdits.length) { + return; + } + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(e.notebook.uri, notebookEdits); + e.waitUntil(Promise.resolve(workspaceEdit)); } })); @@ -229,7 +226,7 @@ export class AttachmentCleaner implements vscode.CodeActionProvider { this.updateDiagnostics(cell.document.uri, diagnostics); - if (cell.index > -1 && !objectEquals(markdownAttachmentsInUse, cell.metadata.attachments)) { + if (cell.index > -1 && !objectEquals(markdownAttachmentsInUse || {}, cell.metadata.attachments || {})) { const updateMetadata: { [key: string]: any } = deepClone(cell.metadata); if (Object.keys(markdownAttachmentsInUse).length === 0) { updateMetadata.attachments = undefined; diff --git a/code/extensions/ipynb/src/notebookImagePaste.ts b/code/extensions/ipynb/src/notebookImagePaste.ts index 94292c26a74..7ea63e7026a 100644 --- a/code/extensions/ipynb/src/notebookImagePaste.ts +++ b/code/extensions/ipynb/src/notebookImagePaste.ts @@ -48,14 +48,15 @@ function getImageMimeType(uri: vscode.Uri): string | undefined { class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider { - public readonly id = 'insertAttachment'; + public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'image', 'attachment'); async provideDocumentPasteEdits( document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, - ): Promise { + ): Promise { const enabled = vscode.workspace.getConfiguration('ipynb', document).get('pasteImagesAsAttachments.enabled', true); if (!enabled) { return; @@ -66,10 +67,10 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod return; } - const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment')); - pasteEdit.yieldTo = [{ mimeType: MimeType.plain }]; + const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment'), DropOrPasteEditProvider.kind); + pasteEdit.yieldTo = [vscode.DocumentPasteEditKind.Empty.append('text')]; pasteEdit.additionalEdit = insert.additionalEdit; - return pasteEdit; + return [pasteEdit]; } async provideDocumentDropEdits( @@ -84,9 +85,9 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod } const dropEdit = new vscode.DocumentDropEdit(insert.insertText); - dropEdit.yieldTo = [{ mimeType: MimeType.plain }]; + dropEdit.yieldTo = [vscode.DocumentPasteEditKind.Empty.append('text')]; dropEdit.additionalEdit = insert.additionalEdit; - dropEdit.label = vscode.l10n.t('Insert Image as Attachment'); + dropEdit.title = vscode.l10n.t('Insert Image as Attachment'); return dropEdit; } @@ -299,14 +300,14 @@ export function notebookImagePasteSetup(): vscode.Disposable { const provider = new DropOrPasteEditProvider(); return vscode.Disposable.from( vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, { - id: provider.id, + providedPasteEditKinds: [DropOrPasteEditProvider.kind], pasteMimeTypes: [ MimeType.png, MimeType.uriList, ], }), vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, { - id: provider.id, + providedDropEditKinds: [DropOrPasteEditProvider.kind], dropMimeTypes: [ ...Object.values(imageExtToMime), MimeType.uriList, diff --git a/code/extensions/ipynb/src/notebookModelStoreSync.ts b/code/extensions/ipynb/src/notebookModelStoreSync.ts new file mode 100644 index 00000000000..535a76c3dfb --- /dev/null +++ b/code/extensions/ipynb/src/notebookModelStoreSync.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext, NotebookCellKind, NotebookDocument, NotebookDocumentChangeEvent, NotebookEdit, workspace, WorkspaceEdit, type NotebookCell, type NotebookDocumentWillSaveEvent } from 'vscode'; +import { getCellMetadata, getVSCodeCellLanguageId, removeVSCodeCellLanguageId, setVSCodeCellLanguageId } from './serializers'; +import { CellMetadata } from './common'; +import { getNotebookMetadata } from './notebookSerializer'; +import type * as nbformat from '@jupyterlab/nbformat'; + +const noop = () => { + // +}; + +/** + * Code here is used to ensure the Notebook Model is in sync the the ipynb JSON file. + * E.g. assume you add a new cell, this new cell will not have any metadata at all. + * However when we save the ipynb, the metadata will be an empty object `{}`. + * Now thats completely different from the metadata os being `empty/undefined` in the model. + * As a result, when looking at things like diff view or accessing metadata, we'll see differences. +* +* This code ensures that the model is in sync with the ipynb file. +*/ +export const pendingNotebookCellModelUpdates = new WeakMap>>(); +export function activate(context: ExtensionContext) { + workspace.onDidChangeNotebookDocument(onDidChangeNotebookCells, undefined, context.subscriptions); + workspace.onWillSaveNotebookDocument(waitForPendingModelUpdates, undefined, context.subscriptions); +} + +function isSupportedNotebook(notebook: NotebookDocument) { + return notebook.notebookType === 'jupyter-notebook' || notebook.notebookType === 'interactive'; +} + +function waitForPendingModelUpdates(e: NotebookDocumentWillSaveEvent) { + if (!isSupportedNotebook(e.notebook)) { + return; + } + + const promises = pendingNotebookCellModelUpdates.get(e.notebook); + if (!promises) { + return; + } + e.waitUntil(Promise.all(promises)); +} + +function cleanup(notebook: NotebookDocument, promise: PromiseLike) { + const pendingUpdates = pendingNotebookCellModelUpdates.get(notebook); + if (pendingUpdates) { + pendingUpdates.delete(promise); + if (!pendingUpdates.size) { + pendingNotebookCellModelUpdates.delete(notebook); + } + } +} +function trackAndUpdateCellMetadata(notebook: NotebookDocument, cell: NotebookCell, metadata: CellMetadata & { vscode?: { languageId: string } }) { + const pendingUpdates = pendingNotebookCellModelUpdates.get(notebook) ?? new Set>(); + pendingNotebookCellModelUpdates.set(notebook, pendingUpdates); + const edit = new WorkspaceEdit(); + edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, { ...(cell.metadata), custom: metadata })]); + const promise = workspace.applyEdit(edit).then(noop, noop); + pendingUpdates.add(promise); + const clean = () => cleanup(notebook, promise); + promise.then(clean, clean); +} + +function onDidChangeNotebookCells(e: NotebookDocumentChangeEvent) { + if (!isSupportedNotebook(e.notebook)) { + return; + } + + const notebook = e.notebook; + const notebookMetadata = getNotebookMetadata(e.notebook); + + // use the preferred language from document metadata or the first cell language as the notebook preferred cell language + const preferredCellLanguage = notebookMetadata.metadata?.language_info?.name; + + // When we change the language of a cell, + // Ensure the metadata in the notebook cell has been updated as well, + // Else model will be out of sync with ipynb https://github.com/microsoft/vscode/issues/207968#issuecomment-2002858596 + e.cellChanges.forEach(e => { + if (!preferredCellLanguage || e.cell.kind !== NotebookCellKind.Code) { + return; + } + const languageIdInMetadata = getVSCodeCellLanguageId(getCellMetadata(e.cell)); + if (e.cell.document.languageId !== preferredCellLanguage && e.cell.document.languageId !== languageIdInMetadata) { + const metadata: CellMetadata = JSON.parse(JSON.stringify(getCellMetadata(e.cell))); + metadata.metadata = metadata.metadata || {}; + setVSCodeCellLanguageId(metadata, e.cell.document.languageId); + trackAndUpdateCellMetadata(notebook, e.cell, metadata); + + } else if (e.cell.document.languageId === preferredCellLanguage && languageIdInMetadata) { + const metadata: CellMetadata = JSON.parse(JSON.stringify(getCellMetadata(e.cell))); + metadata.metadata = metadata.metadata || {}; + removeVSCodeCellLanguageId(metadata); + trackAndUpdateCellMetadata(notebook, e.cell, metadata); + } else if (e.cell.document.languageId === preferredCellLanguage && e.cell.document.languageId === languageIdInMetadata) { + const metadata: CellMetadata = JSON.parse(JSON.stringify(getCellMetadata(e.cell))); + metadata.metadata = metadata.metadata || {}; + removeVSCodeCellLanguageId(metadata); + trackAndUpdateCellMetadata(notebook, e.cell, metadata); + } + }); + + // Ensure all new cells in notebooks with nbformat >= 4.5 have an id. + // Details of the spec can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html# + e.contentChanges.forEach(change => { + change.addedCells.forEach(cell => { + // When ever a cell is added, always update the metadata + // as metadata is always an empty `{}` in ipynb JSON file + const cellMetadata = getCellMetadata(cell); + + // Avoid updating the metadata if it's not required. + if (cellMetadata.metadata) { + if (!isCellIdRequired(notebookMetadata)) { + return; + } + if (isCellIdRequired(notebookMetadata) && cellMetadata?.id) { + return; + } + } + + // Don't edit the metadata directly, always get a clone (prevents accidental singletons and directly editing the objects). + const metadata: CellMetadata = { ...JSON.parse(JSON.stringify(cellMetadata || {})) }; + metadata.metadata = metadata.metadata || {}; + + if (isCellIdRequired(notebookMetadata) && !cellMetadata?.id) { + metadata.id = generateCellId(e.notebook); + } + trackAndUpdateCellMetadata(notebook, cell, metadata); + }); + }); +} + + +/** + * Cell ids are required in notebooks only in notebooks with nbformat >= 4.5 + */ +function isCellIdRequired(metadata: Pick, 'nbformat' | 'nbformat_minor'>) { + if ((metadata.nbformat || 0) >= 5) { + return true; + } + if ((metadata.nbformat || 0) === 4 && (metadata.nbformat_minor || 0) >= 5) { + return true; + } + return false; +} + +function generateCellId(notebook: NotebookDocument) { + while (true) { + // Details of the id can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#adding-an-id-field, + // & here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#updating-older-formats + const id = generateUuid().replace(/-/g, '').substring(0, 8); + let duplicate = false; + for (let index = 0; index < notebook.cellCount; index++) { + const cell = notebook.cellAt(index); + const existingId = getCellMetadata(cell)?.id; + if (!existingId) { + continue; + } + if (existingId === id) { + duplicate = true; + break; + } + } + if (!duplicate) { + return id; + } + } +} + + +/** + * Copied from src/vs/base/common/uuid.ts + */ +function generateUuid() { + // use `randomValues` if possible + function getRandomValues(bucket: Uint8Array): Uint8Array { + for (let i = 0; i < bucket.length; i++) { + bucket[i] = Math.floor(Math.random() * 256); + } + return bucket; + } + + // prep-work + const _data = new Uint8Array(16); + const _hex: string[] = []; + for (let i = 0; i < 256; i++) { + _hex.push(i.toString(16).padStart(2, '0')); + } + + // get data + getRandomValues(_data); + + // set version bits + _data[6] = (_data[6] & 0x0f) | 0x40; + _data[8] = (_data[8] & 0x3f) | 0x80; + + // print as string + let i = 0; + let result = ''; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + return result; +} diff --git a/code/extensions/ipynb/src/serializers.ts b/code/extensions/ipynb/src/serializers.ts index 27c45bce918..e5843239d59 100644 --- a/code/extensions/ipynb/src/serializers.ts +++ b/code/extensions/ipynb/src/serializers.ts @@ -5,7 +5,7 @@ import type * as nbformat from '@jupyterlab/nbformat'; import { NotebookCell, NotebookCellData, NotebookCellKind, NotebookCellOutput } from 'vscode'; -import { CellOutputMetadata } from './common'; +import { CellOutputMetadata, type CellMetadata } from './common'; import { textMimeTypes } from './deserializers'; const textDecoder = new TextDecoder(); @@ -54,28 +54,41 @@ export function sortObjectPropertiesRecursively(obj: any): any { return obj; } -export function getCellMetadata(cell: NotebookCell | NotebookCellData) { - return { +export function getCellMetadata(cell: NotebookCell | NotebookCellData): CellMetadata { + const metadata = { // it contains the cell id, and the cell metadata, along with other nb cell metadata - ...(cell.metadata?.custom ?? {}), - // promote the cell attachments to the top level - attachments: cell.metadata?.custom?.attachments ?? cell.metadata?.attachments + ...(cell.metadata?.custom ?? {}) }; + + // promote the cell attachments to the top level + const attachments = cell.metadata?.custom?.attachments ?? cell.metadata?.attachments; + if (attachments) { + metadata.attachments = attachments; + } + return metadata; +} + +export function getVSCodeCellLanguageId(metadata: CellMetadata): string | undefined { + return metadata.metadata?.vscode?.languageId; +} +export function setVSCodeCellLanguageId(metadata: CellMetadata, languageId: string) { + metadata.metadata = metadata.metadata || {}; + metadata.metadata.vscode = { languageId }; +} +export function removeVSCodeCellLanguageId(metadata: CellMetadata) { + if (metadata.metadata?.vscode) { + delete metadata.metadata.vscode; + } } function createCodeCellFromNotebookCell(cell: NotebookCellData, preferredLanguage: string | undefined): nbformat.ICodeCell { - const cellMetadata = getCellMetadata(cell); - let metadata = cellMetadata?.metadata || {}; // This cannot be empty. + const cellMetadata: CellMetadata = JSON.parse(JSON.stringify(getCellMetadata(cell))); + cellMetadata.metadata = cellMetadata.metadata || {}; // This cannot be empty. if (cell.languageId !== preferredLanguage) { - metadata = { - ...metadata, - vscode: { - languageId: cell.languageId - } - }; + setVSCodeCellLanguageId(cellMetadata, cell.languageId); } else { // cell current language is the same as the preferred cell language in the document, flush the vscode custom language id metadata - metadata.vscode = undefined; + removeVSCodeCellLanguageId(cellMetadata); } const codeCell: any = { @@ -83,7 +96,7 @@ function createCodeCellFromNotebookCell(cell: NotebookCellData, preferredLanguag execution_count: cell.executionSummary?.executionOrder ?? null, source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')), outputs: (cell.outputs || []).map(translateCellDisplayOutput), - metadata: metadata + metadata: cellMetadata.metadata }; if (cellMetadata?.id) { codeCell.id = cellMetadata.id; diff --git a/code/extensions/ipynb/src/test/notebookModelStoreSync.test.ts b/code/extensions/ipynb/src/test/notebookModelStoreSync.test.ts new file mode 100644 index 00000000000..8c1370703e1 --- /dev/null +++ b/code/extensions/ipynb/src/test/notebookModelStoreSync.test.ts @@ -0,0 +1,551 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as sinon from 'sinon'; +import { CancellationTokenSource, Disposable, EventEmitter, ExtensionContext, NotebookCellKind, NotebookDocumentChangeEvent, NotebookDocumentWillSaveEvent, NotebookEdit, NotebookRange, TextDocumentSaveReason, workspace, type CancellationToken, type NotebookCell, type NotebookDocument, type WorkspaceEdit, type WorkspaceEditMetadata } from 'vscode'; +import { activate } from '../notebookModelStoreSync'; + +suite('Notebook Model Store Sync', () => { + let disposables: Disposable[] = []; + let onDidChangeNotebookDocument: EventEmitter; + let onWillSaveNotebookDocument: AsyncEmitter; + let notebook: NotebookDocument; + let token: CancellationTokenSource; + let editsApplied: WorkspaceEdit[] = []; + let pendingPromises: Promise[] = []; + let cellMetadataUpdates: NotebookEdit[] = []; + let applyEditStub: sinon.SinonStub<[edit: WorkspaceEdit, metadata?: WorkspaceEditMetadata | undefined], Thenable>; + setup(() => { + disposables = []; + notebook = { + notebookType: '', + metadata: {} + } as NotebookDocument; + token = new CancellationTokenSource(); + disposables.push(token); + sinon.stub(notebook, 'notebookType').get(() => 'jupyter-notebook'); + applyEditStub = sinon.stub(workspace, 'applyEdit').callsFake((edit: WorkspaceEdit) => { + editsApplied.push(edit); + return Promise.resolve(true); + }); + const context = { subscriptions: [] as Disposable[] } as ExtensionContext; + onDidChangeNotebookDocument = new EventEmitter(); + disposables.push(onDidChangeNotebookDocument); + onWillSaveNotebookDocument = new AsyncEmitter(); + + sinon.stub(NotebookEdit, 'updateCellMetadata').callsFake((index, metadata) => { + const edit = (NotebookEdit.updateCellMetadata as any).wrappedMethod.call(NotebookEdit, index, metadata); + cellMetadataUpdates.push(edit); + return edit; + } + ); + sinon.stub(workspace, 'onDidChangeNotebookDocument').callsFake(cb => + onDidChangeNotebookDocument.event(cb) + ); + sinon.stub(workspace, 'onWillSaveNotebookDocument').callsFake(cb => + onWillSaveNotebookDocument.event(cb) + ); + activate(context); + }); + teardown(async () => { + await Promise.allSettled(pendingPromises); + editsApplied = []; + pendingPromises = []; + cellMetadataUpdates = []; + disposables.forEach(d => d.dispose()); + disposables = []; + sinon.restore(); + }); + + test('Empty cell will not result in any updates', async () => { + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 0); + }); + test('Adding cell for non Jupyter Notebook will not result in any updates', async () => { + sinon.stub(notebook, 'notebookType').get(() => 'some-other-type'); + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: {}, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [ + { + range: new NotebookRange(0, 0), + removedCells: [], + addedCells: [cell] + } + ], + cellChanges: [] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 0); + assert.strictEqual(cellMetadataUpdates.length, 0); + }); + test('Adding cell will result in an update to the metadata', async () => { + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: {}, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [ + { + range: new NotebookRange(0, 0), + removedCells: [], + addedCells: [cell] + } + ], + cellChanges: [] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata; + assert.deepStrictEqual(newMetadata, { custom: { metadata: {} } }); + }); + test('Add cell id if nbformat is 4.5', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: {}, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [ + { + range: new NotebookRange(0, 0), + removedCells: [], + addedCells: [cell] + } + ], + cellChanges: [] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + assert.strictEqual(Object.keys(newMetadata).length, 1); + assert.strictEqual(Object.keys(newMetadata.custom).length, 2); + assert.deepStrictEqual(newMetadata.custom.metadata, {}); + assert.ok(newMetadata.custom.id); + }); + test('Do not add cell id if one already exists', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: { + custom: { + id: '1234' + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [ + { + range: new NotebookRange(0, 0), + removedCells: [], + addedCells: [cell] + } + ], + cellChanges: [] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + assert.strictEqual(Object.keys(newMetadata).length, 1); + assert.strictEqual(Object.keys(newMetadata.custom).length, 2); + assert.deepStrictEqual(newMetadata.custom.metadata, {}); + assert.strictEqual(newMetadata.custom.id, '1234'); + }); + test('Do not perform any updates if cell id and metadata exists', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: { + custom: { + id: '1234', + metadata: {} + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [ + { + range: new NotebookRange(0, 0), + removedCells: [], + addedCells: [cell] + } + ], + cellChanges: [] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 0); + assert.strictEqual(cellMetadataUpdates.length, 0); + }); + test('Store language id in custom metadata, whilst preserving existing metadata', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'python' } + } + } + })); + const cell: NotebookCell = { + document: { + languageId: 'javascript' + } as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: { + custom: { + id: '1234', + metadata: { + collapsed: true, scrolled: true + } + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [ + { + cell, + document: undefined, + metadata: undefined, + outputs: undefined, + executionSummary: undefined + } + ] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + assert.strictEqual(Object.keys(newMetadata).length, 1); + assert.strictEqual(Object.keys(newMetadata.custom).length, 2); + assert.deepStrictEqual(newMetadata.custom.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'javascript' } }); + assert.strictEqual(newMetadata.custom.id, '1234'); + }); + test('No changes when language is javascript', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'javascript' } + } + } + })); + const cell: NotebookCell = { + document: { + languageId: 'javascript' + } as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: { + custom: { + id: '1234', + metadata: { + collapsed: true, scrolled: true + } + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [ + { + cell, + document: undefined, + metadata: undefined, + outputs: undefined, + executionSummary: undefined + } + ] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 0); + assert.strictEqual(cellMetadataUpdates.length, 0); + }); + test('Remove language from metadata when cell language matches kernel language', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'javascript' } + } + } + })); + const cell: NotebookCell = { + document: { + languageId: 'javascript' + } as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: { + custom: { + id: '1234', + metadata: { + vscode: { languageId: 'python' }, + collapsed: true, scrolled: true + } + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [ + { + cell, + document: undefined, + metadata: undefined, + outputs: undefined, + executionSummary: undefined + } + ] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + assert.strictEqual(Object.keys(newMetadata).length, 1); + assert.strictEqual(Object.keys(newMetadata.custom).length, 2); + assert.deepStrictEqual(newMetadata.custom.metadata, { collapsed: true, scrolled: true }); + assert.strictEqual(newMetadata.custom.id, '1234'); + }); + test('Update language in metadata', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'javascript' } + } + } + })); + const cell: NotebookCell = { + document: { + languageId: 'powershell' + } as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: { + custom: { + id: '1234', + metadata: { + vscode: { languageId: 'python' }, + collapsed: true, scrolled: true + } + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [ + { + cell, + document: undefined, + metadata: undefined, + outputs: undefined, + executionSummary: undefined + } + ] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + assert.strictEqual(Object.keys(newMetadata).length, 1); + assert.strictEqual(Object.keys(newMetadata.custom).length, 2); + assert.deepStrictEqual(newMetadata.custom.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'powershell' } }); + assert.strictEqual(newMetadata.custom.id, '1234'); + }); + + test('Will save event without any changes', async () => { + await onWillSaveNotebookDocument.fireAsync({ notebook, reason: TextDocumentSaveReason.Manual }, token.token); + }); + test('Wait for pending updates to complete when saving', async () => { + let resolveApplyEditPromise: (value: boolean) => void; + const promise = new Promise((resolve) => resolveApplyEditPromise = resolve); + applyEditStub.restore(); + sinon.stub(workspace, 'applyEdit').callsFake((edit: WorkspaceEdit) => { + editsApplied.push(edit); + return promise; + }); + + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: {}, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [ + { + range: new NotebookRange(0, 0), + removedCells: [], + addedCells: [cell] + } + ], + cellChanges: [] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + + // Try to save. + let saveCompleted = false; + const saved = onWillSaveNotebookDocument.fireAsync({ + notebook, + reason: TextDocumentSaveReason.Manual + }, token.token); + saved.finally(() => saveCompleted = true); + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Verify we have not yet completed saving. + assert.strictEqual(saveCompleted, false); + + resolveApplyEditPromise!(true); + await new Promise((resolve) => setTimeout(resolve, 1)); + + // Should have completed saving. + saved.finally(() => saveCompleted = true); + }); + + interface IWaitUntil { + token: CancellationToken; + waitUntil(thenable: Promise): void; + } + + interface IWaitUntil { + token: CancellationToken; + waitUntil(thenable: Promise): void; + } + type IWaitUntilData = Omit, 'token'>; + + class AsyncEmitter { + private listeners: ((d: T) => void)[] = []; + get event(): (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => Disposable { + + return (listener, thisArgs, _disposables) => { + this.listeners.push(listener.bind(thisArgs)); + return { + dispose: () => { + // + } + }; + }; + } + dispose() { + this.listeners = []; + } + async fireAsync(data: IWaitUntilData, token: CancellationToken): Promise { + if (!this.listeners.length) { + return; + } + + const promises: Promise[] = []; + this.listeners.forEach(cb => { + const event = { + ...data, + token, + waitUntil: (thenable: Promise) => { + promises.push(thenable); + } + } as T; + cb(event); + }); + + await Promise.all(promises); + } + } +}); diff --git a/code/extensions/javascript/javascript-language-configuration.json b/code/extensions/javascript/javascript-language-configuration.json index 4029985233a..12f6e5cac1f 100644 --- a/code/extensions/javascript/javascript-language-configuration.json +++ b/code/extensions/javascript/javascript-language-configuration.json @@ -111,7 +111,7 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]].*$" + "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" diff --git a/code/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/code/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f7c87fbf9fa..f78f494d727 100644 --- a/code/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/code/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; +import { Disposable, ExtensionContext, Uri, l10n, window } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable } from '../jsonClient'; +import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -43,7 +43,10 @@ export async function activate(context: ExtensionContext) { } }; - client = await startClient(context, newLanguageClient, { schemaRequests, timer }); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); + + client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel }); } catch (e) { console.log(e); diff --git a/code/extensions/json-language-features/client/src/jsonClient.ts b/code/extensions/json-language-features/client/src/jsonClient.ts index ce81dcb4c9e..0a3bffefeb7 100644 --- a/code/extensions/json-language-features/client/src/jsonClient.ts +++ b/code/extensions/json-language-features/client/src/jsonClient.ts @@ -6,12 +6,12 @@ export type JSONLanguageStatus = { schemas: string[] }; import { - workspace, window, languages, commands, OutputChannel, ExtensionContext, extensions, Uri, ColorInformation, + workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n } from 'vscode'; import { - LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, + LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind, DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams, DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient, ProvideFoldingRangeSignature, ProvideDocumentSymbolsSignature, ProvideDocumentColorsSignature } from 'vscode-languageclient'; @@ -130,6 +130,7 @@ export interface Runtime { readonly timer: { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; }; + logOutputChannel: LogOutputChannel; } export interface SchemaRequestService { @@ -150,12 +151,10 @@ export interface AsyncDisposable { } export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { - const outputChannel = window.createOutputChannel(languageServerDescription); - const languageParticipants = getLanguageParticipants(); context.subscriptions.push(languageParticipants); - let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); let restartTrigger: Disposable | undefined; languageParticipants.onDidChange(() => { @@ -164,12 +163,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } restartTrigger = runtime.timer.setTimeout(async () => { if (client) { - outputChannel.appendLine('Extensions have changed, restarting JSON server...'); - outputChannel.appendLine(''); + runtime.logOutputChannel.info('Extensions have changed, restarting JSON server...'); + runtime.logOutputChannel.info(''); const oldClient = client; client = undefined; await oldClient.dispose(); - client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); } }, 2000); }); @@ -178,12 +177,11 @@ export async function startClient(context: ExtensionContext, newLanguageClient: dispose: async () => { restartTrigger?.dispose(); await client?.dispose(); - outputChannel.dispose(); } }; } -async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise { +async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { const toDispose: Disposable[] = []; @@ -232,6 +230,21 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa } })); + function filterSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] { + const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); + if (schemaErrorIndex !== -1) { + const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; + fileSchemaErrors.set(uri.toString(), schemaResolveDiagnostic.message); + if (!schemaDownloadEnabled) { + diagnostics = diagnostics.filter(d => !isSchemaResolveError(d)); + } + if (window.activeTextEditor && window.activeTextEditor.document.uri.toString() === uri.toString()) { + schemaResolutionErrorStatusBarItem.show(); + } + } + return diagnostics; + } + // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for json documents @@ -250,25 +263,17 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa workspace: { didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() }) }, - handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => { - const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); - - if (schemaErrorIndex === -1) { - fileSchemaErrors.delete(uri.toString()); - return next(uri, diagnostics); + provideDiagnostics: async (uriOrDoc, previousResolutId, token, next) => { + const diagnostics = await next(uriOrDoc, previousResolutId, token); + console.log('provideDiagnostics', diagnostics, uriOrDoc); + if (diagnostics && diagnostics.kind === DocumentDiagnosticReportKind.Full) { + const uri = uriOrDoc instanceof Uri ? uriOrDoc : uriOrDoc.uri; + diagnostics.items = filterSchemaErrorDiagnostics(uri, diagnostics.items); } - - const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; - fileSchemaErrors.set(uri.toString(), schemaResolveDiagnostic.message); - - if (!schemaDownloadEnabled) { - diagnostics = diagnostics.filter(d => !isSchemaResolveError(d)); - } - - if (window.activeTextEditor && window.activeTextEditor.document.uri.toString() === uri.toString()) { - schemaResolutionErrorStatusBarItem.show(); - } - + return diagnostics; + }, + handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => { + diagnostics = filterSchemaErrorDiagnostics(uri, diagnostics); next(uri, diagnostics); }, // testing the replace / insert mode @@ -348,7 +353,7 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa } }; - clientOptions.outputChannel = outputChannel; + clientOptions.outputChannel = runtime.logOutputChannel; // Create the language client and start the client. const client = newLanguageClient('json', languageServerDescription, clientOptions); client.registerProposedFeatures(); diff --git a/code/extensions/json-language-features/client/src/node/jsonClientMain.ts b/code/extensions/json-language-features/client/src/node/jsonClientMain.ts index 79d66e32dda..d57ebf80834 100644 --- a/code/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/code/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; +import { Disposable, ExtensionContext, LogOutputChannel, window, l10n, env, LogLevel } from 'vscode'; import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; @@ -14,15 +14,16 @@ import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-li import TelemetryReporter from '@vscode/extension-telemetry'; import { JSONSchemaCache } from './schemaCache'; -let telemetry: TelemetryReporter | undefined; let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { const clientPackageJSON = await getPackageInfo(context); - telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + const telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + context.subscriptions.push(telemetry); - const outputChannel = window.createOutputChannel(languageServerDescription); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/jsonServerMain`; const serverModule = context.asAbsolutePath(serverMain); @@ -38,11 +39,8 @@ export async function activate(context: ExtensionContext) { }; const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - clientOptions.outputChannel = outputChannel; return new LanguageClient(id, name, serverOptions, clientOptions); }; - const log = getLog(outputChannel); - context.subscriptions.push(log); const timer = { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { @@ -54,9 +52,9 @@ export async function activate(context: ExtensionContext) { // pass the location of the localization bundle to the server process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; - const schemaRequests = await getSchemaRequestService(context, log); + const schemaRequests = await getSchemaRequestService(context, logOutputChannel); - client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer }); + client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel }); } export async function deactivate(): Promise { @@ -64,7 +62,6 @@ export async function deactivate(): Promise { await client.dispose(); client = undefined; } - telemetry?.dispose(); } interface IPackageInfo { @@ -84,36 +81,9 @@ async function getPackageInfo(context: ExtensionContext): Promise } } -interface Log { - trace(message: string): void; - isTrace(): boolean; - dispose(): void; -} - -const traceSetting = 'json.trace.server'; -function getLog(outputChannel: OutputChannel): Log { - let trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - const configListener = workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(traceSetting)) { - trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - } - }); - return { - trace(message: string) { - if (trace) { - outputChannel.appendLine(message); - } - }, - isTrace() { - return trace; - }, - dispose: () => configListener.dispose() - }; -} - const retryTimeoutInHours = 2 * 24; // 2 days -async function getSchemaRequestService(context: ExtensionContext, log: Log): Promise { +async function getSchemaRequestService(context: ExtensionContext, log: LogOutputChannel): Promise { let cache: JSONSchemaCache | undefined = undefined; const globalStorage = context.globalStorageUri; @@ -191,7 +161,7 @@ async function getSchemaRequestService(context: ExtensionContext, log: Log): Pro if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) { const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours); if (content) { - if (log.isTrace()) { + if (log.logLevel === LogLevel.Trace) { log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`); } diff --git a/code/extensions/latex/cgmanifest.json b/code/extensions/latex/cgmanifest.json index cd025113ad7..965df91bed4 100644 --- a/code/extensions/latex/cgmanifest.json +++ b/code/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "69915f318570484ef40ed8798c73c63c58704183" + "commitHash": "45c7b12ee68563afd50407e5eac02d30d33dbe7a" } }, "license": "MIT", - "version": "1.5.4", + "version": "1.6.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/code/extensions/markdown-basics/cgmanifest.json b/code/extensions/markdown-basics/cgmanifest.json index bf4ee5e89be..60c6b192bed 100644 --- a/code/extensions/markdown-basics/cgmanifest.json +++ b/code/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "0b36cbbf917fb0188e1a1bafc8287c7abf8b0b37" + "commitHash": "f75d5f55730e72ee7ff386841949048b2395e440" } }, "license": "MIT", diff --git a/code/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/code/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index b5472c56cfd..c84c468b80c 100644 --- a/code/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/code/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/0b36cbbf917fb0188e1a1bafc8287c7abf8b0b37", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/f75d5f55730e72ee7ff386841949048b2395e440", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -1257,7 +1257,7 @@ ] }, "fenced_code_block_powershell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|,|\\{|\\?)[^`]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1|pwsh)((\\s+|:|,|\\{|\\?)[^`]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { diff --git a/code/extensions/markdown-language-features/package.json b/code/extensions/markdown-language-features/package.json index 8d81b3fcc03..a80b0294786 100644 --- a/code/extensions/markdown-language-features/package.json +++ b/code/extensions/markdown-language-features/package.json @@ -466,10 +466,20 @@ "description": "%markdown.server.log.desc%" }, "markdown.editor.drop.enabled": { - "type": "boolean", - "default": true, + "type": "string", + "scope": "resource", "markdownDescription": "%configuration.markdown.editor.drop.enabled%", - "scope": "resource" + "default": "smart", + "enum": [ + "always", + "smart", + "never" + ], + "markdownEnumDescriptions": [ + "%configuration.markdown.editor.drop.enabled.always%", + "%configuration.markdown.editor.drop.enabled.smart%", + "%configuration.markdown.editor.drop.enabled.never%" + ] }, "markdown.editor.drop.copyIntoWorkspace": { "type": "string", @@ -488,7 +498,17 @@ "type": "boolean", "scope": "resource", "markdownDescription": "%configuration.markdown.editor.filePaste.enabled%", - "default": true + "default": "smart", + "enum": [ + "always", + "smart", + "never" + ], + "markdownEnumDescriptions": [ + "%configuration.markdown.editor.filePaste.enabled.always%", + "%configuration.markdown.editor.filePaste.enabled.smart%", + "%configuration.markdown.editor.filePaste.enabled.never%" + ] }, "markdown.editor.filePaste.copyIntoWorkspace": { "type": "string", diff --git a/code/extensions/markdown-language-features/package.nls.json b/code/extensions/markdown-language-features/package.nls.json index af77144f15c..2307358b749 100644 --- a/code/extensions/markdown-language-features/package.nls.json +++ b/code/extensions/markdown-language-features/package.nls.json @@ -38,8 +38,14 @@ "configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.", "configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.", "configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.", + "configuration.markdown.editor.drop.always": "Always insert Markdown links.", + "configuration.markdown.editor.drop.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.", + "configuration.markdown.editor.drop.never": "Never create Markdown links.", "configuration.markdown.editor.drop.copyIntoWorkspace": "Controls if files outside of the workspace that are dropped into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied dropped files should be created", "configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.", + "configuration.markdown.editor.filePaste.always": "Always insert Markdown links.", + "configuration.markdown.editor.filePaste.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.", + "configuration.markdown.editor.filePaste.never": "Never create Markdown links.", "configuration.markdown.editor.filePaste.copyIntoWorkspace": "Controls if files outside of the workspace that are pasted into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied files should be created.", "configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.", "configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.", @@ -64,7 +70,18 @@ "configuration.markdown.updateLinksOnFileMove.include.property": "The glob pattern to match file paths against. Set to true to enable the pattern.", "configuration.markdown.updateLinksOnFileMove.enableForDirectories": "Enable updating links when a directory is moved or renamed in the workspace.", "configuration.markdown.occurrencesHighlight.enabled": "Enable highlighting link occurrences in the current document.", - "configuration.markdown.copyFiles.destination": "Defines where files copied created by drop or paste should be created. This is a map from globs that match on the Markdown document to destinations.\n\nThe destinations may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.", + "configuration.markdown.copyFiles.destination": { + "message": "Configures the path and file name of files created by copy/paste or drag and drop. This is a map of globs that match against a Markdown document path to the destination path where the new file should be created.\n\nThe destination path may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.", + "comment": [ + "This setting is use the user drops or pastes image data into the editor. In this case, VS Code automatically creates a new image file in the workspace containing the dropped/pasted image.", + "It's easier to explain this setting with an example. For example, let's say the setting value was:", + "", + "{ 'docs/*.md': '${documentDirName}/images/${fileName}' }", + "", + "Here the setting is an object mapping from a set of globs to a set of file destinations.", + "The left hand side ('docs/*.md') is a glob that matches against a markdown document. If the glob, matches then we use the right hand side to compute the new file's path and name. The right hand side can also use the special variables document in this setting description." + ] + }, "configuration.markdown.copyFiles.overwriteBehavior": "Controls if files created by drop or paste should overwrite existing files.", "configuration.markdown.copyFiles.overwriteBehavior.nameIncrementally": "If a file with the same name already exists, append a number to the file name, for example: `image.png` becomes `image-1.png`.", "configuration.markdown.copyFiles.overwriteBehavior.overwrite": "If a file with the same name already exists, overwrite it.", diff --git a/code/extensions/markdown-language-features/server/build/pipeline.yml b/code/extensions/markdown-language-features/server/build/pipeline.yml index c229f78cfbf..0c9e3bdbd11 100644 --- a/code/extensions/markdown-language-features/server/build/pipeline.yml +++ b/code/extensions/markdown-language-features/server/build/pipeline.yml @@ -32,3 +32,4 @@ extends: displayName: Compile publishPackage: ${{ parameters.publishPackage }} + packagePlatform: 'Windows' diff --git a/code/extensions/markdown-language-features/server/package.json b/code/extensions/markdown-language-features/server/package.json index 5c389810979..175620ee8de 100644 --- a/code/extensions/markdown-language-features/server/package.json +++ b/code/extensions/markdown-language-features/server/package.json @@ -18,7 +18,7 @@ "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", - "vscode-markdown-languageservice": "^0.4.0", + "vscode-markdown-languageservice": "^0.5.0-alpha.1", "vscode-uri": "^3.0.7" }, "devDependencies": { diff --git a/code/extensions/markdown-language-features/server/yarn.lock b/code/extensions/markdown-language-features/server/yarn.lock index f3a8efb6023..d630fdb5e88 100644 --- a/code/extensions/markdown-language-features/server/yarn.lock +++ b/code/extensions/markdown-language-features/server/yarn.lock @@ -103,6 +103,11 @@ vscode-jsonrpc@8.1.0: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94" integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw== +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + vscode-languageserver-protocol@3.17.3: version "3.17.3" resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz#6d0d54da093f0c0ee3060b81612cce0f11060d57" @@ -111,6 +116,14 @@ vscode-languageserver-protocol@3.17.3: vscode-jsonrpc "8.1.0" vscode-languageserver-types "3.17.3" +vscode-languageserver-protocol@^3.17.1: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + vscode-languageserver-textdocument@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" @@ -121,6 +134,11 @@ vscode-languageserver-types@3.17.3, vscode-languageserver-types@^3.17.3: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64" integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA== +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + vscode-languageserver@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz#5024253718915d84576ce6662dd46a791498d827" @@ -128,16 +146,15 @@ vscode-languageserver@^8.1.0: dependencies: vscode-languageserver-protocol "3.17.3" -vscode-markdown-languageservice@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.4.0.tgz#1ccca383703d38043b58e096fd3224796a2b2ab5" - integrity sha512-3C8pZlC0ofHEYmWwHgenxL6//XrpkrgyytrqNpMlft46q9uBxSUfcXtEGt7wIDNLWsvmgqPqHBwEnBFtLwrWFA== +vscode-markdown-languageservice@^0.5.0-alpha.1: + version "0.5.0-alpha.1" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.1.tgz#0b03f1f8e853e20352587a8de6a2f10f8d14c81c" + integrity sha512-7uVtRSr4+/xlsml9QtBkBAHPmtBv71CKEj6zhPTERYZEOHCwFsue1EHkESWBOGuqQ1NSLXPnHWENcDh5VD+bNw== dependencies: "@vscode/l10n" "^0.0.10" node-html-parser "^6.1.5" picomatch "^2.3.1" - vscode-languageserver-textdocument "^1.0.8" - vscode-languageserver-types "^3.17.3" + vscode-languageserver-protocol "^3.17.1" vscode-uri "^3.0.7" vscode-uri@^3.0.7: diff --git a/code/extensions/markdown-language-features/src/extension.shared.ts b/code/extensions/markdown-language-features/src/extension.shared.ts index d6499591039..e8758bad832 100644 --- a/code/extensions/markdown-language-features/src/extension.shared.ts +++ b/code/extensions/markdown-language-features/src/extension.shared.ts @@ -58,7 +58,7 @@ function registerMarkdownLanguageFeatures( // Language features registerDiagnosticSupport(selector, commandManager), registerFindFileReferenceSupport(commandManager, client), - registerResourceDropOrPasteSupport(selector), + registerResourceDropOrPasteSupport(selector, parser), registerPasteUrlSupport(selector, parser), registerUpdateLinksOnRename(client), ); diff --git a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts index 41c8bd13146..73a012dacec 100644 --- a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts +++ b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts @@ -4,12 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { IMdParser } from '../../markdownEngine'; import { coalesce } from '../../util/arrays'; import { getParentDocumentUri } from '../../util/document'; import { Mime, mediaMimes } from '../../util/mimes'; import { Schemes } from '../../util/schemes'; import { NewFilePathGenerator } from './newFilePathGenerator'; -import { createInsertUriListEdit, createUriListSnippet, getSnippetLabel } from './shared'; +import { DropOrPasteEdit, createInsertUriListEdit, createUriListSnippet, getSnippetLabel } from './shared'; +import { InsertMarkdownLink, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste'; +import { UriList } from '../../util/uriList'; + +enum CopyFilesSettings { + Never = 'never', + MediaFiles = 'mediaFiles', +} /** * Provides support for pasting or dropping resources into markdown documents. @@ -22,7 +30,7 @@ import { createInsertUriListEdit, createUriListSnippet, getSnippetLabel } from ' */ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider { - public static readonly id = 'insertResource'; + public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'link'); public static readonly mimeTypes = [ Mime.textUriList, @@ -31,129 +39,150 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v ]; private readonly _yieldTo = [ - { mimeType: 'text/plain' }, - { extensionId: 'vscode.ipynb', providerId: 'insertAttachment' }, + vscode.DocumentPasteEditKind.Empty.append('text'), + vscode.DocumentPasteEditKind.Empty.append('markdown', 'image', 'attachment'), ]; + constructor( + private readonly _parser: IMdParser, + ) { } + public async provideDocumentDropEdits( document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken, ): Promise { - const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true); - if (!enabled) { - return; - } - - const filesEdit = await this._getMediaFilesDropEdit(document, dataTransfer, token); - if (filesEdit) { - return filesEdit; - } + const edit = await this._createEdit(document, [new vscode.Range(position, position)], dataTransfer, { + insert: this._getEnabled(document, 'editor.drop.enabled'), + copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get('editor.drop.copyIntoWorkspace', CopyFilesSettings.MediaFiles) + }, undefined, token); - if (token.isCancellationRequested) { + if (!edit || token.isCancellationRequested) { return; } - return this._createEditFromUriListData(document, [new vscode.Range(position, position)], dataTransfer, token); + const dropEdit = new vscode.DocumentDropEdit(edit.snippet); + dropEdit.title = edit.label; + dropEdit.kind = ResourcePasteOrDropProvider.kind; + dropEdit.additionalEdit = edit.additionalEdits; + dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo]; + return dropEdit; } public async provideDocumentPasteEdits( document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, - ): Promise { - const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.filePaste.enabled', true); - if (!enabled) { + ): Promise { + const edit = await this._createEdit(document, ranges, dataTransfer, { + insert: this._getEnabled(document, 'editor.paste.enabled'), + copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get('editor.paste.copyIntoWorkspace', CopyFilesSettings.MediaFiles) + }, context, token); + + if (!edit || token.isCancellationRequested) { return; } - const createEdit = await this._getMediaFilesPasteEdit(document, dataTransfer, token); - if (createEdit) { - return createEdit; - } + const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, ResourcePasteOrDropProvider.kind); + pasteEdit.additionalEdit = edit.additionalEdits; + pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo]; + return [pasteEdit]; + } - if (token.isCancellationRequested) { - return; + private _getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink { + const setting = vscode.workspace.getConfiguration('markdown', document).get(settingName, true); + // Convert old boolean values to new enum setting + if (setting === false) { + return InsertMarkdownLink.Never; + } else if (setting === true) { + return InsertMarkdownLink.Smart; + } else { + return setting; } - - return this._createEditFromUriListData(document, ranges, dataTransfer, token); } - private async _createEditFromUriListData( + private async _createEdit( document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + settings: { + insert: InsertMarkdownLink; + copyIntoWorkspace: CopyFilesSettings; + }, + context: vscode.DocumentPasteEditContext | undefined, token: vscode.CancellationToken, - ): Promise { - const uriList = await dataTransfer.get(Mime.textUriList)?.asString(); - if (!uriList || token.isCancellationRequested) { + ): Promise { + if (settings.insert === InsertMarkdownLink.Never) { return; } - const pasteEdit = createInsertUriListEdit(document, ranges, uriList); - if (!pasteEdit) { + let edit = await this._createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token); + if (token.isCancellationRequested) { return; } - const uriEdit = new vscode.DocumentPasteEdit('', pasteEdit.label); - const edit = new vscode.WorkspaceEdit(); - edit.set(document.uri, pasteEdit.edits); - uriEdit.additionalEdit = edit; - uriEdit.yieldTo = this._yieldTo; - return uriEdit; - } - - private async _getMediaFilesPasteEdit( - document: vscode.TextDocument, - dataTransfer: vscode.DataTransfer, - token: vscode.CancellationToken, - ): Promise { - if (getParentDocumentUri(document.uri).scheme === Schemes.untitled) { - return; + if (!edit) { + edit = await this._createEditFromUriListData(document, ranges, dataTransfer, context, token); } - const copyFilesIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.filePaste.copyIntoWorkspace', 'mediaFiles'); - if (copyFilesIntoWorkspace !== 'mediaFiles') { + if (!edit || token.isCancellationRequested) { return; } - const edit = await this._createEditForMediaFiles(document, dataTransfer, token); - if (!edit) { - return; + if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) { + edit.yieldTo.push(vscode.DocumentPasteEditKind.Empty.append('uri')); } - const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label); - pasteEdit.additionalEdit = edit.additionalEdits; - pasteEdit.yieldTo = this._yieldTo; - return pasteEdit; + return edit; } - private async _getMediaFilesDropEdit( + private async _createEditFromUriListData( document: vscode.TextDocument, + ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + context: vscode.DocumentPasteEditContext | undefined, token: vscode.CancellationToken, - ): Promise { - if (getParentDocumentUri(document.uri).scheme === Schemes.untitled) { + ): Promise { + const uriListData = await dataTransfer.get(Mime.textUriList)?.asString(); + if (!uriListData || token.isCancellationRequested) { return; } - const copyIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.drop.copyIntoWorkspace', 'mediaFiles'); - if (copyIntoWorkspace !== 'mediaFiles') { + const uriList = UriList.from(uriListData); + if (!uriList.entries.length) { return; } - const edit = await this._createEditForMediaFiles(document, dataTransfer, token); + // Disable ourselves if there's also a text entry with the same content as our list, + // unless we are explicitly requested. + if (uriList.entries.length === 1 && !context?.only?.contains(ResourcePasteOrDropProvider.kind)) { + const text = await dataTransfer.get(Mime.textPlain)?.asString(); + if (token.isCancellationRequested) { + return; + } + + if (text && textMatchesUriList(text, uriList)) { + return; + } + } + + const edit = createInsertUriListEdit(document, ranges, uriList); if (!edit) { return; } - const dropEdit = new vscode.DocumentDropEdit(edit.snippet); - dropEdit.label = edit.label; - dropEdit.additionalEdit = edit.additionalEdits; - dropEdit.yieldTo = this._yieldTo; - return dropEdit; + const additionalEdits = new vscode.WorkspaceEdit(); + additionalEdits.set(document.uri, edit.edits); + + return { + label: edit.label, + snippet: new vscode.SnippetString(''), + additionalEdits, + yieldTo: [] + }; } /** @@ -164,8 +193,13 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v private async _createEditForMediaFiles( document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, + copyIntoWorkspace: CopyFilesSettings, token: vscode.CancellationToken, - ): Promise<{ snippet: vscode.SnippetString; label: string; additionalEdits: vscode.WorkspaceEdit } | undefined> { + ): Promise { + if (copyIntoWorkspace !== CopyFilesSettings.MediaFiles || getParentDocumentUri(document.uri).scheme === Schemes.untitled) { + return; + } + interface FileEntry { readonly uri: vscode.Uri; readonly newFile?: { readonly contents: vscode.DataTransferFile; readonly overwrite: boolean }; @@ -200,37 +234,51 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v return; } - const workspaceEdit = new vscode.WorkspaceEdit(); + const snippet = createUriListSnippet(document.uri, fileEntries); + if (!snippet) { + return; + } + + const additionalEdits = new vscode.WorkspaceEdit(); for (const entry of fileEntries) { if (entry.newFile) { - workspaceEdit.createFile(entry.uri, { + additionalEdits.createFile(entry.uri, { contents: entry.newFile.contents, overwrite: entry.newFile.overwrite, }); } } - const snippet = createUriListSnippet(document.uri, fileEntries); - if (!snippet) { - return; - } - return { snippet: snippet.snippet, label: getSnippetLabel(snippet), - additionalEdits: workspaceEdit, + additionalEdits, + yieldTo: [], }; } } -export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector): vscode.Disposable { +function textMatchesUriList(text: string, uriList: UriList): boolean { + if (text === uriList.entries[0].str) { + return true; + } + + try { + const uri = vscode.Uri.parse(text); + return uriList.entries.some(entry => entry.uri.toString() === uri.toString()); + } catch { + return false; + } +} + +export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector, parser: IMdParser): vscode.Disposable { return vscode.Disposable.from( - vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(), { - id: ResourcePasteOrDropProvider.id, + vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(parser), { + providedPasteEditKinds: [ResourcePasteOrDropProvider.kind], pasteMimeTypes: ResourcePasteOrDropProvider.mimeTypes, }), - vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(), { - id: ResourcePasteOrDropProvider.id, + vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(parser), { + providedDropEditKinds: [ResourcePasteOrDropProvider.kind], dropMimeTypes: ResourcePasteOrDropProvider.mimeTypes, }), ); diff --git a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index a57f0d39005..7fcff576a3d 100644 --- a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts @@ -5,22 +5,10 @@ import * as vscode from 'vscode'; import { IMdParser } from '../../markdownEngine'; -import { ITextDocument } from '../../types/textDocument'; import { Mime } from '../../util/mimes'; -import { Schemes } from '../../util/schemes'; import { createInsertUriListEdit } from './shared'; - -export enum PasteUrlAsMarkdownLink { - Always = 'always', - SmartWithSelection = 'smartWithSelection', - Smart = 'smart', - Never = 'never' -} - -function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument): PasteUrlAsMarkdownLink { - return vscode.workspace.getConfiguration('markdown', document) - .get('editor.pasteUrlAsFormattedLink.enabled', PasteUrlAsMarkdownLink.SmartWithSelection); -} +import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste'; +import { UriList } from '../../util/uriList'; /** * Adds support for pasting text uris to create markdown links. @@ -29,7 +17,7 @@ function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument): Paste */ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { - public static readonly id = 'insertMarkdownLink'; + public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'link'); public static readonly pasteMimeTypes = [Mime.textPlain]; @@ -41,10 +29,12 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, - ): Promise { - const pasteUrlSetting = getPasteUrlAsFormattedLinkSetting(document); - if (pasteUrlSetting === PasteUrlAsMarkdownLink.Never) { + ): Promise { + const pasteUrlSetting = vscode.workspace.getConfiguration('markdown', document) + .get('editor.pasteUrlAsFormattedLink.enabled', InsertMarkdownLink.SmartWithSelection); + if (pasteUrlSetting === InsertMarkdownLink.Never) { return; } @@ -59,192 +49,30 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { return; } - const edit = createInsertUriListEdit(document, ranges, uriText, { preserveAbsoluteUris: true }); + const edit = createInsertUriListEdit(document, ranges, UriList.from(uriText), { preserveAbsoluteUris: true }); if (!edit) { return; } - const pasteEdit = new vscode.DocumentPasteEdit('', edit.label); + const pasteEdit = new vscode.DocumentPasteEdit('', edit.label, PasteUrlEditProvider.kind); const workspaceEdit = new vscode.WorkspaceEdit(); workspaceEdit.set(document.uri, edit.edits); pasteEdit.additionalEdit = workspaceEdit; if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, pasteUrlSetting, ranges, token))) { - pasteEdit.yieldTo = [{ mimeType: Mime.textPlain }]; + pasteEdit.yieldTo = [ + vscode.DocumentPasteEditKind.Empty.append('text'), + vscode.DocumentPasteEditKind.Empty.append('uri') + ]; } - return pasteEdit; + return [pasteEdit]; } } export function registerPasteUrlSupport(selector: vscode.DocumentSelector, parser: IMdParser) { return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteUrlEditProvider(parser), { - id: PasteUrlEditProvider.id, + providedPasteEditKinds: [PasteUrlEditProvider.kind], pasteMimeTypes: PasteUrlEditProvider.pasteMimeTypes, }); } - -const smartPasteLineRegexes = [ - { regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link - { regex: /\$\$[\s\S]*?\$\$/gm }, // In a fenced math block - { regex: /`[^`]*`/g }, // In inline code - { regex: /\$[^$]*\$/g }, // In inline math - { regex: /<[^<>\s]*>/g }, // Autolink - { regex: /^[ ]{0,3}\[\w+\]:\s.*$/g, isWholeLine: true }, // Block link definition (needed as tokens are not generated for these) -]; - -export async function shouldInsertMarkdownLinkByDefault( - parser: IMdParser, - document: ITextDocument, - pasteUrlSetting: PasteUrlAsMarkdownLink, - ranges: readonly vscode.Range[], - token: vscode.CancellationToken, -): Promise { - switch (pasteUrlSetting) { - case PasteUrlAsMarkdownLink.Always: { - return true; - } - case PasteUrlAsMarkdownLink.Smart: { - return checkSmart(); - } - case PasteUrlAsMarkdownLink.SmartWithSelection: { - // At least one range must not be empty - if (!ranges.some(range => document.getText(range).trim().length > 0)) { - return false; - } - // And all ranges must be smart - return checkSmart(); - } - default: { - return false; - } - } - - async function checkSmart(): Promise { - return (await Promise.all(ranges.map(range => shouldSmartPasteForSelection(parser, document, range, token)))).every(x => x); - } -} - -const textTokenTypes = new Set(['paragraph_open', 'inline', 'heading_open', 'ordered_list_open', 'bullet_list_open', 'list_item_open', 'blockquote_open']); - -async function shouldSmartPasteForSelection( - parser: IMdParser, - document: ITextDocument, - selectedRange: vscode.Range, - token: vscode.CancellationToken, -): Promise { - // Disable for multi-line selections - if (selectedRange.start.line !== selectedRange.end.line) { - return false; - } - - const rangeText = document.getText(selectedRange); - // Disable when the selection is already a link - if (findValidUriInText(rangeText)) { - return false; - } - - if (/\[.*\]\(.*\)/.test(rangeText) || /!\[.*\]\(.*\)/.test(rangeText)) { - return false; - } - - // Check if selection is inside a special block level element using markdown engine - const tokens = await parser.tokenize(document); - if (token.isCancellationRequested) { - return false; - } - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - if (!token.map) { - continue; - } - if (token.map[0] <= selectedRange.start.line && token.map[1] > selectedRange.start.line) { - if (!textTokenTypes.has(token.type)) { - return false; - } - } - - // Special case for html such as: - // - // - // | - // - // - // In this case pasting will cause the html block to be created even though the cursor is not currently inside a block - if (token.type === 'html_block' && token.map[1] === selectedRange.start.line) { - const nextToken = tokens.at(i + 1); - // The next token does not need to be a html_block, but it must be on the next line - if (nextToken?.map?.[0] === selectedRange.end.line + 1) { - return false; - } - } - } - - // Run additional regex checks on the current line to check if we are inside an inline element - const line = document.getText(new vscode.Range(selectedRange.start.line, 0, selectedRange.start.line, Number.MAX_SAFE_INTEGER)); - for (const regex of smartPasteLineRegexes) { - for (const match of line.matchAll(regex.regex)) { - if (match.index === undefined) { - continue; - } - - if (regex.isWholeLine) { - return false; - } - - if (selectedRange.start.character > match.index && selectedRange.start.character < match.index + match[0].length) { - return false; - } - } - } - - return true; -} - - -const externalUriSchemes: ReadonlySet = new Set([ - Schemes.http, - Schemes.https, - Schemes.mailto, - Schemes.file, -]); - -export function findValidUriInText(text: string): string | undefined { - const trimmedUrlList = text.trim(); - - if ( - !/^\S+$/.test(trimmedUrlList) // Uri must consist of a single sequence of characters without spaces - || !trimmedUrlList.includes(':') // And it must have colon somewhere for the scheme. We will verify the schema again later - ) { - return; - } - - let uri: vscode.Uri; - try { - uri = vscode.Uri.parse(trimmedUrlList); - } catch { - // Could not parse - return; - } - - // `Uri.parse` is lenient and will return a `file:` uri even for non-uri text such as `abc` - // Make sure that the resolved scheme starts the original text - if (!trimmedUrlList.toLowerCase().startsWith(uri.scheme.toLowerCase() + ':')) { - return; - } - - // Only enable for an allow list of schemes. Otherwise this can be accidentally activated for non-uri text - // such as `c:\abc` or `value:foo` - if (!externalUriSchemes.has(uri.scheme.toLowerCase())) { - return; - } - - // Some part of the uri must not be empty - // This disables the feature for text such as `http:` - if (!uri.authority && uri.path.length < 2 && !uri.query && !uri.fragment) { - return; - } - - return trimmedUrlList; -} diff --git a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index 8bfc9ae2ff5..563c125cfc6 100644 --- a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -7,11 +7,10 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as URI from 'vscode-uri'; import { ITextDocument } from '../../types/textDocument'; -import { coalesce } from '../../util/arrays'; import { getDocumentDir } from '../../util/document'; import { Schemes } from '../../util/schemes'; +import { UriList } from '../../util/uriList'; import { resolveSnippet } from './snippets'; -import { parseUriList } from '../../util/uriList'; enum MediaKind { Image, @@ -68,24 +67,13 @@ export function getSnippetLabel(counter: { insertedAudioVideoCount: number; inse export function createInsertUriListEdit( document: ITextDocument, ranges: readonly vscode.Range[], - urlList: string, + urlList: UriList, options?: UriListSnippetOptions, ): { edits: vscode.SnippetTextEdit[]; label: string } | undefined { - if (!ranges.length) { + if (!ranges.length || !urlList.entries.length) { return; } - const entries = coalesce(parseUriList(urlList).map(line => { - try { - return { uri: vscode.Uri.parse(line), str: line }; - } catch { - // Uri parse failure - return undefined; - } - })); - if (!entries.length) { - return; - } const edits: vscode.SnippetTextEdit[] = []; @@ -94,14 +82,14 @@ export function createInsertUriListEdit( let insertedAudioVideoCount = 0; // Use 1 for all empty ranges but give non-empty range unique indices starting after 1 - let placeHolderStartIndex = 1 + entries.length; + let placeHolderStartIndex = 1 + urlList.entries.length; // Sort ranges by start position const orderedRanges = [...ranges].sort((a, b) => a.start.compareTo(b.start)); const allRangesAreEmpty = orderedRanges.every(range => range.isEmpty); for (const range of orderedRanges) { - const snippet = createUriListSnippet(document.uri, entries, { + const snippet = createUriListSnippet(document.uri, urlList.entries, { placeholderText: range.isEmpty ? undefined : document.getText(range), placeholderStartIndex: allRangesAreEmpty ? 1 : placeHolderStartIndex, ...options, @@ -114,7 +102,7 @@ export function createInsertUriListEdit( insertedImageCount += snippet.insertedImageCount; insertedAudioVideoCount += snippet.insertedAudioVideoCount; - placeHolderStartIndex += entries.length; + placeHolderStartIndex += urlList.entries.length; edits.push(new vscode.SnippetTextEdit(range, snippet.snippet)); } @@ -273,3 +261,10 @@ function needsBracketLink(mdPath: string): boolean { return nestingCount > 0; } + +export interface DropOrPasteEdit { + readonly snippet: vscode.SnippetString; + readonly label: string; + readonly additionalEdits: vscode.WorkspaceEdit; + readonly yieldTo: vscode.DocumentPasteEditKind[]; +} diff --git a/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/smartDropOrPaste.ts b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/smartDropOrPaste.ts new file mode 100644 index 00000000000..deaa4b58212 --- /dev/null +++ b/code/extensions/markdown-language-features/src/languageFeatures/copyFiles/smartDropOrPaste.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IMdParser } from '../../markdownEngine'; +import { ITextDocument } from '../../types/textDocument'; +import { Schemes } from '../../util/schemes'; + +const smartPasteLineRegexes = [ + { regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link + { regex: /\$\$[\s\S]*?\$\$/gm }, // In a fenced math block + { regex: /`[^`]*`/g }, // In inline code + { regex: /\$[^$]*\$/g }, // In inline math + { regex: /<[^<>\s]*>/g }, // Autolink + { regex: /^[ ]{0,3}\[\w+\]:\s.*$/g, isWholeLine: true }, // Block link definition (needed as tokens are not generated for these) +]; + +export async function shouldInsertMarkdownLinkByDefault( + parser: IMdParser, + document: ITextDocument, + pasteUrlSetting: InsertMarkdownLink, + ranges: readonly vscode.Range[], + token: vscode.CancellationToken +): Promise { + switch (pasteUrlSetting) { + case InsertMarkdownLink.Always: { + return true; + } + case InsertMarkdownLink.Smart: { + return checkSmart(); + } + case InsertMarkdownLink.SmartWithSelection: { + // At least one range must not be empty + if (!ranges.some(range => document.getText(range).trim().length > 0)) { + return false; + } + // And all ranges must be smart + return checkSmart(); + } + default: { + return false; + } + } + + async function checkSmart(): Promise { + return (await Promise.all(ranges.map(range => shouldSmartPasteForSelection(parser, document, range, token)))).every(x => x); + } +} + +const textTokenTypes = new Set([ + 'paragraph_open', + 'inline', + 'heading_open', + 'ordered_list_open', + 'bullet_list_open', + 'list_item_open', + 'blockquote_open', +]); + +async function shouldSmartPasteForSelection( + parser: IMdParser, + document: ITextDocument, + selectedRange: vscode.Range, + token: vscode.CancellationToken +): Promise { + // Disable for multi-line selections + if (selectedRange.start.line !== selectedRange.end.line) { + return false; + } + + const rangeText = document.getText(selectedRange); + // Disable when the selection is already a link + if (findValidUriInText(rangeText)) { + return false; + } + + if (/\[.*\]\(.*\)/.test(rangeText) || /!\[.*\]\(.*\)/.test(rangeText)) { + return false; + } + + // Check if selection is inside a special block level element using markdown engine + const tokens = await parser.tokenize(document); + if (token.isCancellationRequested) { + return false; + } + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if (!token.map) { + continue; + } + if (token.map[0] <= selectedRange.start.line && token.map[1] > selectedRange.start.line) { + if (!textTokenTypes.has(token.type)) { + return false; + } + } + + // Special case for html such as: + // + // + // | + // + // + // In this case pasting will cause the html block to be created even though the cursor is not currently inside a block + if (token.type === 'html_block' && token.map[1] === selectedRange.start.line) { + const nextToken = tokens.at(i + 1); + // The next token does not need to be a html_block, but it must be on the next line + if (nextToken?.map?.[0] === selectedRange.end.line + 1) { + return false; + } + } + } + + // Run additional regex checks on the current line to check if we are inside an inline element + const line = document.getText(new vscode.Range(selectedRange.start.line, 0, selectedRange.start.line, Number.MAX_SAFE_INTEGER)); + for (const regex of smartPasteLineRegexes) { + for (const match of line.matchAll(regex.regex)) { + if (match.index === undefined) { + continue; + } + + if (regex.isWholeLine) { + return false; + } + + if (selectedRange.start.character > match.index && selectedRange.start.character < match.index + match[0].length) { + return false; + } + } + } + + return true; +} + +const externalUriSchemes: ReadonlySet = new Set([ + Schemes.http, + Schemes.https, + Schemes.mailto, + Schemes.file, +]); + +export function findValidUriInText(text: string): string | undefined { + const trimmedUrlList = text.trim(); + + if (!/^\S+$/.test(trimmedUrlList) // Uri must consist of a single sequence of characters without spaces + || !trimmedUrlList.includes(':') // And it must have colon somewhere for the scheme. We will verify the schema again later + ) { + return; + } + + let uri: vscode.Uri; + try { + uri = vscode.Uri.parse(trimmedUrlList); + } catch { + // Could not parse + return; + } + + // `Uri.parse` is lenient and will return a `file:` uri even for non-uri text such as `abc` + // Make sure that the resolved scheme starts the original text + if (!trimmedUrlList.toLowerCase().startsWith(uri.scheme.toLowerCase() + ':')) { + return; + } + + // Only enable for an allow list of schemes. Otherwise this can be accidentally activated for non-uri text + // such as `c:\abc` or `value:foo` + if (!externalUriSchemes.has(uri.scheme.toLowerCase())) { + return; + } + + // Some part of the uri must not be empty + // This disables the feature for text such as `http:` + if (!uri.authority && uri.path.length < 2 && !uri.query && !uri.fragment) { + return; + } + + return trimmedUrlList; +} + +export enum InsertMarkdownLink { + Always = 'always', + SmartWithSelection = 'smartWithSelection', + Smart = 'smart', + Never = 'never' +} + diff --git a/code/extensions/markdown-language-features/src/test/pasteUrl.test.ts b/code/extensions/markdown-language-features/src/test/pasteUrl.test.ts index df863a25652..2afa4465f76 100644 --- a/code/extensions/markdown-language-features/src/test/pasteUrl.test.ts +++ b/code/extensions/markdown-language-features/src/test/pasteUrl.test.ts @@ -6,10 +6,11 @@ import * as assert from 'assert'; import 'mocha'; import * as vscode from 'vscode'; import { InMemoryDocument } from '../client/inMemoryDocument'; -import { PasteUrlAsMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from '../languageFeatures/copyFiles/pasteUrlProvider'; import { createInsertUriListEdit } from '../languageFeatures/copyFiles/shared'; -import { createNewMarkdownEngine } from './engine'; +import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from '../languageFeatures/copyFiles/smartDropOrPaste'; import { noopToken } from '../util/cancellation'; +import { UriList } from '../util/uriList'; +import { createNewMarkdownEngine } from './engine'; function makeTestDoc(contents: string) { return new InMemoryDocument(vscode.Uri.file('test.md'), contents); @@ -21,7 +22,7 @@ suite('createEditAddingLinksForUriList', () => { // createEditAddingLinksForUriList -> checkSmartPaste -> tryGetUriListSnippet -> createUriListSnippet -> createLinkSnippet const result = createInsertUriListEdit( - new InMemoryDocument(vscode.Uri.file('test.md'), 'hello world!'), [new vscode.Range(0, 0, 0, 12)], 'https://www.microsoft.com/'); + new InMemoryDocument(vscode.Uri.file('test.md'), 'hello world!'), [new vscode.Range(0, 0, 0, 12)], UriList.from('https://www.microsoft.com/')); // need to check the actual result -> snippet value assert.strictEqual(result?.label, 'Insert Markdown Link'); }); @@ -110,27 +111,27 @@ suite('createEditAddingLinksForUriList', () => { suite('createInsertUriListEdit', () => { test('Should create snippet with < > when pasted link has an mismatched parentheses', () => { - const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.mic(rosoft.com'); + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.mic(rosoft.com')); assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}]()'); }); test('Should create Markdown link snippet when pasteAsMarkdownLink is true', () => { - const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.microsoft.com'); + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.microsoft.com')); assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.microsoft.com)'); }); test('Should use an unencoded URI string in Markdown link when passing in an external browser link', () => { - const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.microsoft.com'); + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.microsoft.com')); assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.microsoft.com)'); }); test('Should not decode an encoded URI string when passing in an external browser link', () => { - const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.microsoft.com/%20'); + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.microsoft.com/%20')); assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.microsoft.com/%20)'); }); test('Should not encode an unencoded URI string when passing in an external browser link', () => { - const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.example.com/path?query=value&another=value#fragment'); + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.example.com/path?query=value&another=value#fragment')); assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.example.com/path?query=value&another=value#fragment)'); }); }); @@ -140,41 +141,41 @@ suite('createEditAddingLinksForUriList', () => { test('Smart should be enabled for selected plain text', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('hello world'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 12)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('hello world'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 12)], noopToken), true); }); test('Smart should be enabled in headers', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('# title'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 2, 0, 2)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('# title'), InsertMarkdownLink.Smart, [new vscode.Range(0, 2, 0, 2)], noopToken), true); }); test('Smart should be enabled in lists', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('1. text'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('1. text'), InsertMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken), true); }); test('Smart should be enabled in blockquotes', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('> text'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('> text'), InsertMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken), true); }); test('Smart should be disabled in indented code blocks', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' code'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 4)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' code'), InsertMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 4)], noopToken), false); }); test('Smart should be disabled in fenced code blocks', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('```\r\n\r\n```'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('```\r\n\r\n```'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken), false); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('~~~\r\n\r\n~~~'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('~~~\r\n\r\n~~~'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken), false); }); @@ -183,127 +184,127 @@ suite('createEditAddingLinksForUriList', () => { const engine = createNewMarkdownEngine(); (await engine.getEngine(undefined)).use(katex); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(engine, makeTestDoc('$$\r\n\r\n$$'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken), + await shouldInsertMarkdownLinkByDefault(engine, makeTestDoc('$$\r\n\r\n$$'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken), false); }); test('Smart should be disabled in link definitions', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: http://example.com'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: http://example.com'), InsertMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken), false); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 7, 0, 7)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), InsertMarkdownLink.Smart, [new vscode.Range(0, 7, 0, 7)], noopToken), false); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken), false); }); test('Smart should be disabled in html blocks', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\na\n

'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\na\n

'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), false); }); test('Smart should be disabled in html blocks where paste creates the block', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\n\n

'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\n\n

'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), false, 'Between two html tags should be treated as html block'); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\n\ntext'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\n\ntext'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), false, 'Between opening html tag and text should be treated as html block'); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\n\n\n

'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('

\n\n\n

'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), true, 'Extra new line after paste should not be treated as html block'); }); test('Smart should be disabled in Markdown links', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[a](bcdef)'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[a](bcdef)'), InsertMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken), false); }); test('Smart should be disabled in Markdown images', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('![a](bcdef)'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 10)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('![a](bcdef)'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 10)], noopToken), false); }); test('Smart should be disabled in inline code', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), InsertMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken), false, 'Should be disabled inside of inline code'); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken), true, 'Should be enabled when cursor is outside but next to inline code'); assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`a`'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`a`'), InsertMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken), true, 'Should be enabled when cursor is outside but next to inline code'); }); test('Smart should be enabled when pasting over inline code ', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`xyz`'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 5)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`xyz`'), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 5)], noopToken), true); }); test('Smart should be disabled in inline math', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('$$'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 1)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('$$'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 1)], noopToken), false); }); test('Smart should be enabled for empty selection', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken), true); }); test('SmartWithSelection should disable for empty selection', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 0)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 0)], noopToken), false); }); test('Smart should disable for selected link', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('https://www.microsoft.com'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 25)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('https://www.microsoft.com'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 25)], noopToken), false); }); test('Smart should disable for selected link with trailing whitespace', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' https://www.microsoft.com '), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 30)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' https://www.microsoft.com '), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 30)], noopToken), false); }); test('Should evaluate pasteAsMarkdownLink as true for a link pasted in square brackets', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[abc]'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 4)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[abc]'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 4)], noopToken), true); }); test('Should evaluate pasteAsMarkdownLink as false for selected whitespace and new lines', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' \r\n\r\n'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 7)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' \r\n\r\n'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 7)], noopToken), false); }); test('Smart should be disabled inside of autolinks', async () => { assert.strictEqual( - await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<>'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken), + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<>'), InsertMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken), false); }); }); diff --git a/code/extensions/markdown-language-features/src/util/uriList.ts b/code/extensions/markdown-language-features/src/util/uriList.ts index 04897af453e..8b7f52e568f 100644 --- a/code/extensions/markdown-language-features/src/util/uriList.ts +++ b/code/extensions/markdown-language-features/src/util/uriList.ts @@ -3,12 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from './arrays'; +import * as vscode from 'vscode'; + function splitUriList(str: string): string[] { return str.split('\r\n'); } -export function parseUriList(str: string): string[] { +function parseUriList(str: string): string[] { return splitUriList(str) .filter(value => !value.startsWith('#')) // Remove comments .map(value => value.trim()); } + +export class UriList { + + static from(str: string): UriList { + return new UriList(coalesce(parseUriList(str).map(line => { + try { + return { uri: vscode.Uri.parse(line), str: line }; + } catch { + // Uri parse failure + return undefined; + } + }))); + } + + private constructor( + public readonly entries: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str: string }> + ) { } +} diff --git a/code/extensions/package.json b/code/extensions/package.json index 4365c20acc1..ab93f194b51 100644 --- a/code/extensions/package.json +++ b/code/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "5.3.2" + "typescript": "5.4" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/code/extensions/razor/cgmanifest.json b/code/extensions/razor/cgmanifest.json index e90c7d75d8c..d3685974bdb 100644 --- a/code/extensions/razor/cgmanifest.json +++ b/code/extensions/razor/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/razor", "repositoryUrl": "https://github.com/dotnet/razor", - "commitHash": "b44d0a906d054d2d343adc3f58cbea11d97d7488" + "commitHash": "f01e110af179981942987384d2b5d4e489eab014" } }, "license": "MIT", diff --git a/code/extensions/razor/syntaxes/cshtml.tmLanguage.json b/code/extensions/razor/syntaxes/cshtml.tmLanguage.json index 4594037960a..389a6daf249 100644 --- a/code/extensions/razor/syntaxes/cshtml.tmLanguage.json +++ b/code/extensions/razor/syntaxes/cshtml.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/razor/commit/b44d0a906d054d2d343adc3f58cbea11d97d7488", + "version": "https://github.com/dotnet/razor/commit/f01e110af179981942987384d2b5d4e489eab014", "name": "ASP.NET Razor", "scopeName": "text.html.cshtml", "patterns": [ @@ -527,6 +527,15 @@ }, { "include": "#using-directive" + }, + { + "include": "#rendermode-directive" + }, + { + "include": "#preservewhitespace-directive" + }, + { + "include": "#typeparam-directive" } ] }, @@ -851,6 +860,75 @@ } } }, + "rendermode-directive": { + "name": "meta.directive", + "match": "(@)(rendermode)\\s+([^$]+)?", + "captures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.razor.directive.rendermode" + }, + "3": { + "patterns": [ + { + "include": "source.cs#type" + } + ] + } + } + }, + "preservewhitespace-directive": { + "name": "meta.directive", + "match": "(@)(preservewhitespace)\\s+([^$]+)?", + "captures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.razor.directive.preservewhitespace" + }, + "3": { + "patterns": [ + { + "include": "source.cs#boolean-literal" + } + ] + } + } + }, + "typeparam-directive": { + "name": "meta.directive", + "match": "(@)(typeparam)\\s+([^$]+)?", + "captures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "keyword.control.razor.directive.typeparam" + }, + "3": { + "patterns": [ + { + "include": "source.cs#type" + } + ] + } + } + }, "attribute-directive": { "name": "meta.directive", "begin": "(@)(attribute)\\b\\s+", diff --git a/code/extensions/scss/cgmanifest.json b/code/extensions/scss/cgmanifest.json index 12247769ce2..a67a4f54609 100644 --- a/code/extensions/scss/cgmanifest.json +++ b/code/extensions/scss/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "atom/language-sass", "repositoryUrl": "https://github.com/atom/language-sass", - "commitHash": "f52ab12f7f9346cc2568129d8c4419bd3d506b47" + "commitHash": "303bbf0c250fe380b9e57375598cfd916110758b" } }, "license": "MIT", "description": "The file syntaxes/scss.json was derived from the Atom package https://github.com/atom/language-sass which was originally converted from the TextMate bundle https://github.com/alexsancho/SASS.tmbundle.", - "version": "0.62.1" + "version": "0.61.4" } ], "version": 1 diff --git a/code/extensions/shellscript/cgmanifest.json b/code/extensions/shellscript/cgmanifest.json index dbb4301b62c..d12320c1b95 100644 --- a/code/extensions/shellscript/cgmanifest.json +++ b/code/extensions/shellscript/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jeff-hykin/better-shell-syntax", "repositoryUrl": "https://github.com/jeff-hykin/better-shell-syntax", - "commitHash": "a3de7b32f1537194a83ee848838402fbf4b67424" + "commitHash": "4ba5d703087cac3c60cd57b206fd1cea0ff959cc" } }, "license": "MIT", - "version": "1.6.2" + "version": "1.7.1" } ], "version": 1 diff --git a/code/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json b/code/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json index 9950c577c48..21766dc8477 100644 --- a/code/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json +++ b/code/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/a3de7b32f1537194a83ee848838402fbf4b67424", + "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/4ba5d703087cac3c60cd57b206fd1cea0ff959cc", "name": "Shell Script", "scopeName": "source.shell", "patterns": [ @@ -14,34 +14,59 @@ ], "repository": { "alias_statement": { - "begin": "(alias)[ \\t]*+[ \\t]*+(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))[ \\t]*+)?((?\\(\\)\\$`\\\\\"\\|]+(?!>))", + "match": "(?:[ \\t]*+)((?:[^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+)(?!>))", "captures": { "1": { "name": "string.unquoted.argument.shell", @@ -116,67 +141,108 @@ } ] }, - "assignment": { + "array_value": { + "begin": "(?:[ \\t]*+)(?:(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))(?:[ \\t]*+)((?:(?:((?|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/))(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))|((?!\"|'|\\\\\\n?$)[^!'\" \\t\\n\\r]+?))(?:(?= |\\t)|(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/)))(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))|((?!\"|'|\\\\\\n?$)(?:[^!'\" \\t\\n\\r]+?)))(?:(?= |\\t)|(?:(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/))(?!\\\\\\n?$)", + "begin": "(?:(?:[ \\t]*+)(?:(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/))(?!\\\\\\n?$)))", "end": "(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?)", + "match": "(?<==| |\\t|^|\\{|\\(|\\[)(?:(?:(?:(?:(?:(0[xX][0-9A-Fa-f]+)|(0\\d+))|(\\d{1,2}#[0-9a-zA-Z@_]+))|(-?\\d+(?:\\.\\d+)))|(-?\\d+(?:\\.\\d+)+))|(-?\\d+))(?= |\\t|$|\\}|\\)|;)", "captures": { "1": { "name": "constant.numeric.shell constant.numeric.hex.shell" @@ -1531,16 +1757,19 @@ "name": "constant.numeric.shell constant.numeric.other.shell" }, "4": { - "name": "constant.numeric.shell constant.numeric.integer.shell" + "name": "constant.numeric.shell constant.numeric.decimal.shell" }, "5": { + "name": "constant.numeric.shell constant.numeric.version.shell" + }, + "6": { "name": "constant.numeric.shell constant.numeric.integer.shell" } } }, "option": { - "begin": "[ \\t]++(-)((?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t])))", - "end": "(?:(?=[ \\t])|(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?|#|\\n|$|;|[ \\t]))))", + "end": "(?:(?=[ \\t])|(?:(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?>?)(?:[ \\t]*+)([^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+))", + "captures": { + "1": { + "name": "keyword.operator.redirect.shell" + }, + "2": { + "name": "string.unquoted.argument.shell" + } + } + }, "redirect_number": { - "match": "(?<=[ \\t])(?:(1)|(2)|(\\d+))(?=>)", + "match": "(?<=[ \\t])(?:(?:(1)|(2)|(\\d+))(?=>))", "captures": { "1": { "name": "keyword.operator.redirect.stdout.shell" @@ -1687,17 +1927,17 @@ "regexp": { "patterns": [ { - "match": ".+" + "match": "(?:.+)" } ] }, "simple_options": { - "match": "(?:[ \\t]++\\-\\w+)*", + "match": "(?:(?:[ \\t]++)\\-(?:\\w+))*", "captures": { "0": { "patterns": [ { - "match": "[ \\t]++(\\-)(\\w+)", + "match": "(?:[ \\t]++)(\\-)(\\w+)", "captures": { "1": { "name": "string.unquoted.argument.shell constant.other.option.dash.shell" @@ -1711,11 +1951,15 @@ } } }, + "simple_unquoted": { + "match": "[^ \\t\\n'&;<>\\(\\)\\$`\\\\\"\\|]", + "name": "string.unquoted.shell" + }, "start_of_command": { - "match": "[ \\t]*+(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/))(?!\\\\\\n?$)" + "match": "(?:(?:[ \\t]*+)(?:(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/))(?!\\\\\\n?$)))" }, "start_of_double_quoted_command_name": { - "match": "(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?:[ \\t]*+([^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+(?!>)))?(?:(?:\\$\")|\")", + "match": "(?:(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?:(?:(?:[ \\t]*+)((?:[^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+)(?!>)))?)(?:(?:\\$\")|\"))", "captures": { "1": { "name": "entity.name.function.call.shell entity.name.command.shell", @@ -1744,7 +1988,7 @@ "name": "meta.statement.command.name.quoted.shell string.quoted.double.shell punctuation.definition.string.begin.shell entity.name.function.call.shell entity.name.command.shell" }, "start_of_single_quoted_command_name": { - "match": "(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?:[ \\t]*+([^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+(?!>)))?(?:(?:\\$')|')", + "match": "(?:(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?:(?:(?:[ \\t]*+)((?:[^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+)(?!>)))?)(?:(?:\\$')|'))", "captures": { "1": { "name": "entity.name.function.call.shell entity.name.command.shell", @@ -1866,7 +2110,7 @@ "variable": { "patterns": [ { - "match": "(\\$)(\\@(?!\\w))", + "match": "(?:(\\$)(\\@(?!\\w)))", "captures": { "1": { "name": "punctuation.definition.variable.shell variable.parameter.positional.all.shell" @@ -1877,7 +2121,7 @@ } }, { - "match": "(\\$)([0-9](?!\\w))", + "match": "(?:(\\$)([0-9](?!\\w)))", "captures": { "1": { "name": "punctuation.definition.variable.shell variable.parameter.positional.shell" @@ -1888,7 +2132,7 @@ } }, { - "match": "(\\$)([-*#?$!0_](?!\\w))", + "match": "(?:(\\$)([-*#?$!0_](?!\\w)))", "captures": { "1": { "name": "punctuation.definition.variable.shell variable.language.special.shell" @@ -1899,7 +2143,7 @@ } }, { - "begin": "(\\$)(\\{)[ \\t]*+(?=\\d)", + "begin": "(?:(\\$)(\\{)(?:[ \\t]*+)(?=\\d))", "end": "\\}", "beginCaptures": { "1": { @@ -1921,7 +2165,7 @@ "name": "keyword.operator.expansion.shell" }, { - "match": "(\\[)[^\\]]+(\\])", + "match": "(?:(\\[)(?:[^\\]]+)(\\]))", "captures": { "1": { "name": "punctuation.section.array.shell" @@ -1936,7 +2180,7 @@ "name": "variable.parameter.positional.shell" }, { - "match": "(?('autoImportFileExcludePatterns')?.map(p => { // Normalization rules: https://github.com/microsoft/TypeScript/pull/49578 - const slashNormalized = p.replace(/\\/g, '/'); - const isRelative = /^\.\.?($|\/)/.test(slashNormalized); + const isRelative = /^\.\.?($|[\/\\])/.test(p); + // In TypeScript < 5.3, the first path component cannot be a wildcard, so we need to prefix + // it with a path root (e.g. `/` or `c:\`) + const wildcardPrefix = this.client.apiVersion.gte(API.v540) + ? '' + : path.parse(this.client.toTsFilePath(workspaceFolder)!).root; return path.isAbsolute(p) ? p : - p.startsWith('*') ? '/' + slashNormalized : - isRelative ? vscode.Uri.joinPath(workspaceFolder, p).fsPath : - '/**/' + slashNormalized; + p.startsWith('*') ? wildcardPrefix + p : + isRelative ? this.client.toTsFilePath(vscode.Uri.joinPath(workspaceFolder, p))! : + wildcardPrefix + '**' + path.sep + p; }); } } diff --git a/code/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/code/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index a627670c70a..f724cfd8c44 100644 --- a/code/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/code/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -198,10 +198,10 @@ class SupportedCodeActionProvider { private readonly client: ITypeScriptServiceClient ) { } - public async getFixableDiagnosticsForContext(context: vscode.CodeActionContext): Promise { + public async getFixableDiagnosticsForContext(diagnostics: readonly vscode.Diagnostic[]): Promise { const fixableCodes = await this.fixableDiagnosticCodes; return DiagnosticsSet.from( - context.diagnostics.filter(diagnostic => typeof diagnostic.code !== 'undefined' && fixableCodes.has(diagnostic.code + ''))); + diagnostics.filter(diagnostic => typeof diagnostic.code !== 'undefined' && fixableCodes.has(diagnostic.code + ''))); } @memoize @@ -214,6 +214,8 @@ class SupportedCodeActionProvider { class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { + private static readonly _maxCodeActionsPerFile: number = 1000; + public static readonly metadata: vscode.CodeActionProviderMetadata = { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }; @@ -237,7 +239,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { @@ -246,12 +248,32 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { + setTimeout(resolve, 500); + }); + + if (token.isCancellationRequested) { + return; + } + const allDiagnostics: vscode.Diagnostic[] = []; + + // Match ranges again after getting new diagnostics + for (const diagnostic of this.diagnosticsManager.getDiagnostics(document.uri)) { + if (range.intersection(diagnostic.range)) { + const newLen = allDiagnostics.push(diagnostic); + if (newLen > TypeScriptQuickFixProvider._maxCodeActionsPerFile) { + break; + } + } + } + diagnostics = allDiagnostics; } - if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) { + const fixableDiagnostics = await this.supportedCodeActionProvider.getFixableDiagnosticsForContext(diagnostics); + if (!fixableDiagnostics.size || token.isCancellationRequested) { return; } diff --git a/code/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/code/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index dc5948314d9..9f5d76f5ac3 100644 --- a/code/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/code/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -725,6 +725,13 @@ export default class BufferSyncSupport extends Disposable { orderedFileSet.set(buffer.resource, undefined); } + for (const { resource } of orderedFileSet.entries()) { + const buffer = this.syncedBuffers.get(resource); + if (buffer && !this.shouldValidate(buffer)) { + orderedFileSet.delete(resource); + } + } + if (orderedFileSet.size) { const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, orderedFileSet, () => { if (this.pendingGetErr === getErr) { diff --git a/code/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/code/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index b8d863ebb1a..45e09d63481 100644 --- a/code/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/code/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript/lib/tsserverlibrary'; +import type ts from '../../../../node_modules/typescript/lib/typescript'; export = ts.server.protocol; @@ -11,7 +11,7 @@ declare enum ServerType { Semantic = 'semantic', } -declare module 'typescript/lib/tsserverlibrary' { +declare module '../../../../node_modules/typescript/lib/typescript' { namespace server.protocol { type TextInsertion = ts.TextInsertion; type ScriptElementKind = ts.ScriptElementKind; diff --git a/code/extensions/typescript-language-features/src/tsconfig.ts b/code/extensions/typescript-language-features/src/tsconfig.ts index 196cf185170..04f08a128bc 100644 --- a/code/extensions/typescript-language-features/src/tsconfig.ts +++ b/code/extensions/typescript-language-features/src/tsconfig.ts @@ -26,8 +26,8 @@ export function inferredProjectCompilerOptions( serviceConfig: TypeScriptServiceConfiguration, ): Proto.ExternalProjectCompilerOptions { const projectConfig: Proto.ExternalProjectCompilerOptions = { - module: 'ESNext' as Proto.ModuleKind, - moduleResolution: 'Node' as Proto.ModuleResolutionKind, + module: (version.gte(API.v540) ? 'Preserve' : 'ESNext') as Proto.ModuleKind, + moduleResolution: (version.gte(API.v540) ? 'Bundler' : 'Node') as Proto.ModuleResolutionKind, target: 'ES2022' as Proto.ScriptTarget, jsx: 'react' as Proto.JsxEmit, }; diff --git a/code/extensions/typescript-language-features/src/typescriptServiceClient.ts b/code/extensions/typescript-language-features/src/typescriptServiceClient.ts index 0796befc6bb..812a9c457bf 100644 --- a/code/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/code/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -653,7 +653,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (!this._isPromptingAfterCrash) { if (this.pluginManager.plugins.length) { prompt = vscode.window.showWarningMessage( - vscode.l10n.t("The JS/TS language service crashed.\nThis may be caused by a plugin contributed by one of these extensions: {0}.\nPlease try disabling these extensions before filing an issue against VS Code.", pluginExtensionList)); + vscode.l10n.t("The JS/TS language service crashed.\nThis may be caused by a plugin contributed by one of these extensions: {0}.\nPlease try disabling these extensions before filing an issue against VS Code.", pluginExtensionList), reportIssueItem); } else { prompt = vscode.window.showWarningMessage( vscode.l10n.t("The JS/TS language service crashed."), @@ -1039,7 +1039,7 @@ function getReportIssueArgsForError( error: TypeScriptServerError, tsServerLog: TsServerLog | undefined, globalPlugins: readonly TypeScriptServerPlugin[], -): { extensionId: string; issueTitle: string; issueBody: string } | undefined { +): { extensionId: string; issueTitle: string; issueBody: string; issueSource: string; issueData: string } | undefined { if (!error.serverStack || !error.serverMessage) { return undefined; } @@ -1089,19 +1089,20 @@ The log file may contain personal data, including full paths and source code fro After enabling this setting, future crash reports will include the server log.`); } - sections.push(`**TS Server Error Stack** + const serverErrorStack = `**TS Server Error Stack** Server: \`${error.serverId}\` \`\`\` ${error.serverStack} -\`\`\``); +\`\`\``; return { extensionId: 'vscode.typescript-language-features', issueTitle: `TS Server fatal error: ${error.serverMessage}`, - - issueBody: sections.join('\n\n') + issueSource: 'vscode', + issueBody: sections.join('\n\n'), + issueData: serverErrorStack, }; } diff --git a/code/extensions/vscode-api-tests/package.json b/code/extensions/vscode-api-tests/package.json index e9323fc9c43..b0b56a3fdd9 100644 --- a/code/extensions/vscode-api-tests/package.json +++ b/code/extensions/vscode-api-tests/package.json @@ -64,6 +64,19 @@ }, "icon": "media/icon.png", "contributes": { + "chatParticipants": [ + { + "name": "participant", + "description": "test", + "isDefault": true, + "commands": [ + { + "name": "hello", + "description": "Hello" + } + ] + } + ], "configuration": { "type": "object", "title": "Test Config", diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 6fb0262e132..1a07b475074 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; +import { commands, CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; import { DeferredPromise, assertNoRpc, closeAllEditors, disposeAll } from '../utils'; suite('chat', () => { @@ -45,18 +45,13 @@ suite('chat', () => { return null; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; disposables.push(participant); return emitter.event; } test('participant and slash command', async () => { const onRequest = setupParticipant(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); let i = 0; onRequest(request => { @@ -64,7 +59,7 @@ suite('chat', () => { assert.deepStrictEqual(request.request.command, 'hello'); assert.strictEqual(request.request.prompt, 'friend'); i++; - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); } else { assert.strictEqual(request.context.history.length, 1); assert.strictEqual(request.context.history[0].participant.name, 'participant'); @@ -81,7 +76,7 @@ suite('chat', () => { })); const deferred = getDeferredForRequest(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant hi #myVar' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant hi #myVar' }); const request = await deferred.p; assert.strictEqual(request.prompt, 'hi #myVar'); assert.strictEqual(request.variables[0].values[0].value, 'myValue'); @@ -102,20 +97,15 @@ suite('chat', () => { return { metadata: { key: 'value' } }; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; participant.followupProvider = { - provideFollowups(result, _token) { + provideFollowups(result, _context, _token) { deferred.complete(result); return []; }, }; disposables.push(participant); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); const result = await deferred.p; assert.deepStrictEqual(result.metadata, { key: 'value' }); }); diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts index c2cdd073d74..e2145d4ee28 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts @@ -37,7 +37,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed)); } } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -62,7 +62,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed + '\n')); } } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -88,7 +88,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(`(${ranges.length})${selections.join(' ')}`)); } } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); editor.selections = [new vscode.Selection(0, 0, 0, 0)]; @@ -118,7 +118,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('a')); providerAResolve(); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); // Later registered providers will be called first testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { @@ -132,7 +132,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('b')); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -159,7 +159,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('xyz')); providerAResolve(); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { @@ -172,7 +172,7 @@ suite.skip('vscode API - Copy Paste', function () { const str = await entry!.asString(); dataTransfer.set(textPlain, new vscode.DataTransferItem(reverseString(str))); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -192,13 +192,13 @@ suite.skip('vscode API - Copy Paste', function () { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { dataTransfer.set(textPlain, new vscode.DataTransferItem('xyz')); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], _dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { throw new Error('Expected testing error from bad provider'); } - }, { id: 'test', copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts index 4990e30af59..f9d8d6a82db 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts @@ -83,14 +83,14 @@ const apiTestSerializer: vscode.NotebookSerializer = { }, deserializeNotebook(_content, _token) { const dto: vscode.NotebookData = { - metadata: { custom: { testMetadata: false } }, + metadata: { testMetadata: false }, cells: [ { value: 'test', languageId: 'typescript', kind: vscode.NotebookCellKind.Code, outputs: [], - metadata: { custom: { testCellMetadata: 123 } }, + metadata: { testCellMetadata: 123 }, executionSummary: { timing: { startTime: 10, endTime: 20 } } }, { @@ -107,7 +107,7 @@ const apiTestSerializer: vscode.NotebookSerializer = { }) ], executionSummary: { executionOrder: 5, success: true }, - metadata: { custom: { testCellMetadata: 456 } } + metadata: { testCellMetadata: 456 } } ] }; @@ -230,6 +230,30 @@ const apiTestSerializer: vscode.NotebookSerializer = { await closeAllEditors(); }); + test('#207742 - New Untitled notebook failed if previous untilted notebook is modified', async function () { + await vscode.commands.executeCommand('ipynb.newUntitledIpynb'); + assert.notStrictEqual(vscode.window.activeNotebookEditor, undefined, 'untitled notebook editor is not undefined'); + const document = vscode.window.activeNotebookEditor!.notebook; + + // open another text editor + const textDocument = await vscode.workspace.openTextDocument({ language: 'javascript', content: 'let abc = 0;' }); + await vscode.window.showTextDocument(textDocument); + + // insert a new cell to notebook document + const edit = new vscode.WorkspaceEdit(); + const notebookEdit = new vscode.NotebookEdit(new vscode.NotebookRange(1, 1), [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(1)', 'python')]); + edit.set(document.uri, [notebookEdit]); + await vscode.workspace.applyEdit(edit); + + // switch to the notebook editor + await vscode.window.showNotebookDocument(document); + await closeAllEditors(); + await vscode.commands.executeCommand('ipynb.newUntitledIpynb'); + assert.notStrictEqual(vscode.window.activeNotebookEditor, undefined, 'untitled notebook editor is not undefined'); + + await closeAllEditors(); + }); + // TODO: Skipped due to notebook content provider removal test.skip('#115855 onDidSaveNotebookDocument', async function () { const resource = await createRandomNotebookFile(); diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index fe57d7a883c..8d193edcc91 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -295,11 +295,11 @@ suite('Notebook Document', function () { const document = await vscode.workspace.openNotebookDocument(uri); const edit = new vscode.WorkspaceEdit(); - const metdataEdit = vscode.NotebookEdit.updateNotebookMetadata({ ...document.metadata, custom: { ...(document.metadata.custom || {}), extraNotebookMetadata: true } }); + const metdataEdit = vscode.NotebookEdit.updateNotebookMetadata({ ...document.metadata, extraNotebookMetadata: true }); edit.set(document.uri, [metdataEdit]); const success = await vscode.workspace.applyEdit(edit); assert.equal(success, true); - assert.ok(document.metadata.custom.extraNotebookMetadata, `Test metadata not found`); + assert.ok(document.metadata.extraNotebookMetadata, `Test metadata not found`); }); test('setTextDocumentLanguage for notebook cells', async function () { diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index 58297eb4e5b..37e16207ddb 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -91,14 +91,14 @@ const apiTestSerializer: vscode.NotebookSerializer = { }, deserializeNotebook(_content, _token) { const dto: vscode.NotebookData = { - metadata: { custom: { testMetadata: false } }, + metadata: { testMetadata: false }, cells: [ { value: 'test', languageId: 'typescript', kind: vscode.NotebookCellKind.Code, outputs: [], - metadata: { custom: { testCellMetadata: 123 } }, + metadata: { testCellMetadata: 123 }, executionSummary: { timing: { startTime: 10, endTime: 20 } } }, { @@ -115,7 +115,7 @@ const apiTestSerializer: vscode.NotebookSerializer = { }) ], executionSummary: { executionOrder: 5, success: true }, - metadata: { custom: { testCellMetadata: 456 } } + metadata: { testCellMetadata: 456 } } ] }; diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts index ba7ce21e32f..4f8331c286f 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts @@ -139,9 +139,9 @@ suite('vscode API - quick input', function () { }; const quickPick = createQuickPick({ - events: ['active', 'selection', 'accept', 'active', 'selection', 'active', 'selection', 'accept', 'hide'], - activeItems: [['eins'], [], ['drei']], - selectionItems: [['eins'], [], ['drei']], + events: ['active', 'selection', 'accept', 'active', 'selection', 'accept', 'hide'], + activeItems: [['eins'], ['drei']], + selectionItems: [['eins'], ['drei']], acceptedItems: { active: [['eins'], ['drei']], selection: [['eins'], ['drei']], diff --git a/code/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json b/code/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json index c24aadf2ed9..5948ea77fcd 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json +++ b/code/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json @@ -29,7 +29,7 @@ }, { "c": "declare", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell storage.modifier.declare.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell storage.modifier.declare.shell", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -43,7 +43,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.statement.command.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -56,22 +56,8 @@ } }, { - "c": "-", - "t": "source.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", - "r": { - "dark_plus": "constant.other.option: #569CD6", - "light_plus": "constant.other.option: #0000FF", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "constant.other.option: #569CD6", - "hc_light": "string: #0F4A85", - "light_modern": "constant.other.option: #0000FF" - } - }, - { - "c": "A", - "t": "source.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", + "c": "-A", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.unquoted.argument.shell constant.other.option.shell", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -85,7 +71,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -98,22 +84,36 @@ } }, { - "c": "juices=", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.argument.shell", + "c": "juices", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "=", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" } }, { "c": "(", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": "[", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell punctuation.definition.logical-expression.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -154,50 +154,22 @@ } }, { - "c": "'", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell string.quoted.single.shell punctuation.definition.string.begin.shell", - "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" - } - }, - { - "c": "apple", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell string.quoted.single.shell", - "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" - } - }, - { - "c": "'", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "c": "'apple'", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.unquoted.shell entity.other.attribute-name.shell", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #E50000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #E50000", + "hc_black": "entity.other.attribute-name: #9CDCFE", + "dark_modern": "entity.other.attribute-name: #9CDCFE", + "hc_light": "entity.other.attribute-name: #264F78", + "light_modern": "entity.other.attribute-name: #E50000" } }, { "c": "]", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell punctuation.definition.logical-expression.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -211,7 +183,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,49 +197,49 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell meta.statement.command.name.quoted.shell string.quoted.single.shell punctuation.definition.string.begin.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", - "hc_black": "entity.name.function: #DCDCAA", - "dark_modern": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #5E2CBC", - "light_modern": "entity.name.function: #795E26" + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" } }, { "c": "Apple Juice", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell meta.statement.command.name.continuation string.quoted.single entity.name.function.call entity.name.command", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", - "hc_black": "entity.name.function: #DCDCAA", - "dark_modern": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #5E2CBC", - "light_modern": "entity.name.function: #795E26" + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" } }, { "c": "'", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell string.quoted.single.shell punctuation.definition.string.end.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", - "hc_black": "entity.name.function: #DCDCAA", - "dark_modern": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #5E2CBC", - "light_modern": "entity.name.function: #795E26" + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" } }, { "c": " ", - "t": "source.shell meta.statement.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -281,7 +253,7 @@ }, { "c": "[", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell punctuation.definition.logical-expression.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -294,50 +266,22 @@ } }, { - "c": "'", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell string.quoted.single.shell punctuation.definition.string.begin.shell", - "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" - } - }, - { - "c": "orange", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell string.quoted.single.shell", - "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" - } - }, - { - "c": "'", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "c": "'orange'", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.unquoted.shell entity.other.attribute-name.shell", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #E50000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #E50000", + "hc_black": "entity.other.attribute-name: #9CDCFE", + "dark_modern": "entity.other.attribute-name: #9CDCFE", + "hc_light": "entity.other.attribute-name: #264F78", + "light_modern": "entity.other.attribute-name: #E50000" } }, { "c": "]", - "t": "source.shell meta.statement.shell meta.scope.logical-expression.shell punctuation.definition.logical-expression.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -351,7 +295,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -365,49 +309,49 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell meta.statement.command.name.quoted.shell string.quoted.single.shell punctuation.definition.string.begin.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", - "hc_black": "entity.name.function: #DCDCAA", - "dark_modern": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #5E2CBC", - "light_modern": "entity.name.function: #795E26" + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" } }, { "c": "Orange Juice", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell meta.statement.command.name.continuation string.quoted.single entity.name.function.call entity.name.command", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", - "hc_black": "entity.name.function: #DCDCAA", - "dark_modern": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #5E2CBC", - "light_modern": "entity.name.function: #795E26" + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" } }, { "c": "'", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell string.quoted.single.shell punctuation.definition.string.end.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", - "hc_black": "entity.name.function: #DCDCAA", - "dark_modern": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #5E2CBC", - "light_modern": "entity.name.function: #795E26" + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" } }, { "c": ")", - "t": "source.shell meta.statement.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/code/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json b/code/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json index 198ace22005..ef48d804f66 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json +++ b/code/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json @@ -29,7 +29,7 @@ }, { "c": "cmd", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.statement.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -43,7 +43,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.statement.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -57,7 +57,7 @@ }, { "c": "(", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.shell", + "t": "source.shell meta.statement.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.statement.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -85,7 +85,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.begin.shell", + "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -99,7 +99,7 @@ }, { "c": "ls", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell", + "t": "source.shell meta.statement.shell string.quoted.single.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -113,7 +113,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -127,7 +127,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.statement.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.begin.shell", + "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -155,7 +155,7 @@ }, { "c": "-la", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell", + "t": "source.shell meta.statement.shell string.quoted.single.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -169,7 +169,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -183,7 +183,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.statement.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": ")", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.shell", + "t": "source.shell meta.statement.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/code/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json b/code/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json index b725f8255df..9323c747ca8 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json +++ b/code/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json @@ -1946,7 +1946,7 @@ } }, { - "c": " /path/file", + "c": " ", "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", "r": { "dark_plus": "default: #D4D4D4", @@ -1959,6 +1959,20 @@ "light_modern": "default: #3B3B3B" } }, + { + "c": "/path/file", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.argument.shell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, { "c": "\t# A heredoc with a variable ", "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent", @@ -2115,7 +2129,7 @@ }, { "c": "\t", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.command.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2465,7 +2479,7 @@ }, { "c": "export", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.expression.assignment.shell storage.modifier.export.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell storage.modifier.export.shell", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -2479,7 +2493,7 @@ }, { "c": " ", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2493,7 +2507,7 @@ }, { "c": "NODE_ENV", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2507,7 +2521,7 @@ }, { "c": "=", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2521,16 +2535,16 @@ }, { "c": "development", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.expression.assignment.shell string.unquoted.argument.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" } }, { diff --git a/code/extensions/yaml/package.json b/code/extensions/yaml/package.json index 96e02e37da6..5223f71c52b 100644 --- a/code/extensions/yaml/package.json +++ b/code/extensions/yaml/package.json @@ -37,10 +37,10 @@ "yaml" ], "extensions": [ + ".yaml", ".yml", ".eyaml", ".eyml", - ".yaml", ".cff", ".yaml-tmlanguage", ".yaml-tmpreferences", diff --git a/code/extensions/yarn.lock b/code/extensions/yarn.lock index fd4f84c5073..80668d5ab39 100644 --- a/code/extensions/yarn.lock +++ b/code/extensions/yarn.lock @@ -234,10 +234,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43" - integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== +typescript@5.4: + version "5.4.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" + integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/code/package.json b/code/package.json index 8e159684403..73b4be45c34 100644 --- a/code/package.json +++ b/code/package.json @@ -1,7 +1,7 @@ { "name": "che-code", "version": "1.88.0", - "distro": "b314654a31bdba8cd2b0c7548e931916d03416bf", + "distro": "7ca938298e57ad434ea8807e132707055458a749", "author": { "name": "Microsoft Corporation" }, @@ -80,14 +80,14 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.10", + "@xterm/addon-image": "0.8.0-beta.10", + "@xterm/addon-search": "0.15.0-beta.10", + "@xterm/addon-serialize": "0.13.0-beta.10", + "@xterm/addon-unicode11": "0.8.0-beta.10", + "@xterm/addon-webgl": "0.18.0-beta.10", + "@xterm/headless": "5.5.0-beta.10", + "@xterm/xterm": "5.5.0-beta.10", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -97,13 +97,13 @@ "native-is-elevated": "0.7.0", "native-keymap": "^3.3.4", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta6", + "node-pty": "1.1.0-beta11", "tas-client-umd": "0.1.8", "v8-inspect-profiler": "^0.1.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "yauzl": "^2.9.2", + "yauzl": "^3.0.0", "yazl": "^2.4.3", "ws": "8.2.3", "js-yaml": "^4.1.0" @@ -129,7 +129,7 @@ "@types/wicg-file-system-access": "^2020.9.6", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", - "@types/yauzl": "^2.9.1", + "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/experimental-utils": "^5.57.0", @@ -151,7 +151,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "27.3.2", + "electron": "28.2.6", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", @@ -211,7 +211,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.4.0-dev.20240206", + "typescript": "^5.5.0-dev.20240318", "typescript-formatter": "7.1.0", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", diff --git a/code/remote/.yarnrc b/code/remote/.yarnrc index cac528fd2dd..4e7208cdf69 100644 --- a/code/remote/.yarnrc +++ b/code/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" -target "18.17.1" -ms_build_id "255375" +target "18.18.2" +ms_build_id "256117" runtime "node" build_from_source "true" diff --git a/code/remote/package.json b/code/remote/package.json index a8c630f6670..a8400a9d1cb 100644 --- a/code/remote/package.json +++ b/code/remote/package.json @@ -13,14 +13,14 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.10", + "@xterm/addon-image": "0.8.0-beta.10", + "@xterm/addon-search": "0.15.0-beta.10", + "@xterm/addon-serialize": "0.13.0-beta.10", + "@xterm/addon-unicode11": "0.8.0-beta.10", + "@xterm/addon-webgl": "0.18.0-beta.10", + "@xterm/headless": "5.5.0-beta.10", + "@xterm/xterm": "5.5.0-beta.10", "cookie": "^0.4.0", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", @@ -29,12 +29,12 @@ "kerberos": "^2.0.1", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta6", + "node-pty": "1.1.0-beta11", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "yauzl": "^2.9.2", + "yauzl": "^3.0.0", "yazl": "^2.4.3", "ws": "8.2.3", "js-yaml": "^4.1.0" diff --git a/code/remote/web/package.json b/code/remote/web/package.json index e891be054dd..5a1fc1ef3f8 100644 --- a/code/remote/web/package.json +++ b/code/remote/web/package.json @@ -7,13 +7,13 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.10", + "@xterm/addon-image": "0.8.0-beta.10", + "@xterm/addon-search": "0.15.0-beta.10", + "@xterm/addon-serialize": "0.13.0-beta.10", + "@xterm/addon-unicode11": "0.8.0-beta.10", + "@xterm/addon-webgl": "0.18.0-beta.10", + "@xterm/xterm": "5.5.0-beta.10", "jschardet": "3.0.0", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", diff --git a/code/remote/web/yarn.lock b/code/remote/web/yarn.lock index 1af0f2be779..89860646a34 100644 --- a/code/remote/web/yarn.lock +++ b/code/remote/web/yarn.lock @@ -48,40 +48,40 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-canvas@0.7.0-beta.10": + version "0.7.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.10.tgz#034e3db05c4cf0ebf8864af173a50e0e7ef73fde" + integrity sha512-lCrjgVgkW7oom8ABcRYhem0DD2UI2/b5d197tM7S5a809elLAhdgfic7ctnyRGc4i8YgcjCq85gC4Rqm2uAIOw== + +"@xterm/addon-image@0.8.0-beta.10": + version "0.8.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.10.tgz#fd1f41599bf79241c9c6525a445aef8305c692df" + integrity sha512-tJXbOJ9cRcD6c1LuEj45eQTBbpAAelP+0ZB2JYlHflYom/7odwlq/jB/9Z9ZqI4gaIelZKcc8pZ9ENtH6SPBYA== + +"@xterm/addon-search@0.15.0-beta.10": + version "0.15.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.10.tgz#ed7f1a446effd0afde4870e491da86014a2fc0ef" + integrity sha512-k3G4w58WmbgpoRlnV2kVbkIw8is9VMjL4D5+IrvChDAwAQR322yHhBsAgMzKL4x3T29FfDI9R5AV7JpQn8G3RA== + +"@xterm/addon-serialize@0.13.0-beta.10": + version "0.13.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.10.tgz#5d4fac2d5e207749558ac285a39f8d4596b359e3" + integrity sha512-zD90pUnKFOXNaz5AGbtoBC6yVlY7cNxh1L1ziy4PtwccluhNd8u8U72Jo19a75lgRy41r5/2Kxa1TeD9SKMMoA== + +"@xterm/addon-unicode11@0.8.0-beta.10": + version "0.8.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.10.tgz#3494bb92e64942fc420abfe1224458369f83ae50" + integrity sha512-ptbWMOU7qwSYIICqJB5+HUlDJHD4vyk0uIuUQpHZVmN0vj1PnhX96vHQqWXH9ZgEbVYQy0iUfBhfOkVjsYuXTQ== + +"@xterm/addon-webgl@0.18.0-beta.10": + version "0.18.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.10.tgz#603f07112f67084131d8b1529ed5498450ddaa49" + integrity sha512-nHby5VR1fGjvHc1b0DYDro2V2H4GFOpiQXG0e5MkjDGMWQvl/VS7DKoOxsWYw88oIS19H1xTLiPxRFM4v65LFQ== + +"@xterm/xterm@5.5.0-beta.10": + version "5.5.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.10.tgz#b10e8b28c62785d577279e0ced150e942c4679a7" + integrity sha512-yfnPCik1Pqnd7TN7oycg+cqZuAWqqNUnvEzhaBTWdOe7ksCJaoKUgV3yjEM9tQId8dXMcyXqnf7ThnJamvi5CQ== jschardet@3.0.0: version "3.0.0" diff --git a/code/remote/yarn.lock b/code/remote/yarn.lock index 2e539372993..b00a420abe2 100644 --- a/code/remote/yarn.lock +++ b/code/remote/yarn.lock @@ -114,45 +114,45 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-canvas@0.7.0-beta.10": + version "0.7.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.10.tgz#034e3db05c4cf0ebf8864af173a50e0e7ef73fde" + integrity sha512-lCrjgVgkW7oom8ABcRYhem0DD2UI2/b5d197tM7S5a809elLAhdgfic7ctnyRGc4i8YgcjCq85gC4Rqm2uAIOw== + +"@xterm/addon-image@0.8.0-beta.10": + version "0.8.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.10.tgz#fd1f41599bf79241c9c6525a445aef8305c692df" + integrity sha512-tJXbOJ9cRcD6c1LuEj45eQTBbpAAelP+0ZB2JYlHflYom/7odwlq/jB/9Z9ZqI4gaIelZKcc8pZ9ENtH6SPBYA== + +"@xterm/addon-search@0.15.0-beta.10": + version "0.15.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.10.tgz#ed7f1a446effd0afde4870e491da86014a2fc0ef" + integrity sha512-k3G4w58WmbgpoRlnV2kVbkIw8is9VMjL4D5+IrvChDAwAQR322yHhBsAgMzKL4x3T29FfDI9R5AV7JpQn8G3RA== + +"@xterm/addon-serialize@0.13.0-beta.10": + version "0.13.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.10.tgz#5d4fac2d5e207749558ac285a39f8d4596b359e3" + integrity sha512-zD90pUnKFOXNaz5AGbtoBC6yVlY7cNxh1L1ziy4PtwccluhNd8u8U72Jo19a75lgRy41r5/2Kxa1TeD9SKMMoA== + +"@xterm/addon-unicode11@0.8.0-beta.10": + version "0.8.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.10.tgz#3494bb92e64942fc420abfe1224458369f83ae50" + integrity sha512-ptbWMOU7qwSYIICqJB5+HUlDJHD4vyk0uIuUQpHZVmN0vj1PnhX96vHQqWXH9ZgEbVYQy0iUfBhfOkVjsYuXTQ== + +"@xterm/addon-webgl@0.18.0-beta.10": + version "0.18.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.10.tgz#603f07112f67084131d8b1529ed5498450ddaa49" + integrity sha512-nHby5VR1fGjvHc1b0DYDro2V2H4GFOpiQXG0e5MkjDGMWQvl/VS7DKoOxsWYw88oIS19H1xTLiPxRFM4v65LFQ== + +"@xterm/headless@5.5.0-beta.10": + version "5.5.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.10.tgz#28a6803edb925ba6a1e22c94d3acd20ceccc0d87" + integrity sha512-ohtAGfL6GTDn1ZEnzRwBHh7DqGXJhcn9Ta2sbRseB1r70ZB0ClBIIg1ZAn/a1l/VwF/99WtY3VC+zbfFp3mnYA== + +"@xterm/xterm@5.5.0-beta.10": + version "5.5.0-beta.10" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.10.tgz#b10e8b28c62785d577279e0ced150e942c4679a7" + integrity sha512-yfnPCik1Pqnd7TN7oycg+cqZuAWqqNUnvEzhaBTWdOe7ksCJaoKUgV3yjEM9tQId8dXMcyXqnf7ThnJamvi5CQ== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" @@ -443,10 +443,10 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-pty@1.1.0-beta6: - version "1.1.0-beta6" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta6.tgz#8b27ce40268e313868925e1b46f2af98cc677881" - integrity sha512-ZcuPz5wIbfF4rebVv8sl+nf2Cn5dVMqlEl9PtabCt4uIffGDnovOpmwh16Oh/MThrwSmeJL6gBwu6lIbBtW7DQ== +node-pty@1.1.0-beta11: + version "1.1.0-beta11" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta11.tgz#909d5dd8f9aa2a7857e7b632fd4d2d4768bdf69f" + integrity sha512-vTjF+VrvSCfPDILUkIT+YrG1Fdn06/eBRS2fc9a3JzYAvknMB1Ip8aoJhxl8hNpjWAbprmCEiV91mlfNpCD+GQ== dependencies: node-addon-api "^7.1.0" @@ -660,6 +660,14 @@ yauzl@^2.9.2: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" +yauzl@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-3.1.1.tgz#d85503cc34933c0bcb3646ee2b97afedbebe32e7" + integrity sha512-MPxA7oN5cvGV0wzfkeHKF2/+Q4TkMpHSWGRy/96I4Cozljmx0ph91+Muxh6HegEtDC4GftJ8qYDE51vghFiEYA== + dependencies: + buffer-crc32 "~0.2.3" + pend "~1.2.0" + yazl@^2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.4.3.tgz#ec26e5cc87d5601b9df8432dbdd3cd2e5173a071" diff --git a/code/resources/server/bin/helpers/check-requirements-linux.sh b/code/resources/server/bin/helpers/check-requirements-linux.sh index be5207c2f05..a12570797ba 100644 --- a/code/resources/server/bin/helpers/check-requirements-linux.sh +++ b/code/resources/server/bin/helpers/check-requirements-linux.sh @@ -5,6 +5,15 @@ set -e +# The script checks necessary server requirements for the classic server +# scenarios. Currently, the script can exit with any of the following +# 3 exit codes and should be handled accordingly on the extension side. +# +# 0: All requirements are met, use the default server. +# 99: Unsupported OS, abort server startup with appropriate error message. +# 100: Use legacy server. +# + # Do not remove this check. # Provides a way to skip the server requirements check from # outside the install flow. A system process can create this @@ -19,6 +28,12 @@ if [ -f "/tmp/vscode-skip-server-requirements-check" ]; then exit 0 fi +# Default to legacy server if the following file is present. +if [ -f "$HOME/@@SERVER_APPLICATION_NAME@@-use-legacy" ]; then + echo "!!! WARNING: Using legacy server due to the presence of $HOME/@@SERVER_APPLICATION_NAME@@-use-legacy !!!" + exit 100 +fi + ARCH=$(uname -m) found_required_glibc=0 found_required_glibcxx=0 @@ -144,7 +159,7 @@ else fi if [ "$found_required_glibc" = "0" ] || [ "$found_required_glibcxx" = "0" ]; then - echo "Error: Missing required dependencies. Please refer to our FAQ https://aka.ms/vscode-remote/faq/old-linux for additional information." + echo "Warning: Missing required dependencies. Please refer to our FAQ https://aka.ms/vscode-remote/faq/old-linux for additional information." # Custom exit code based on https://tldp.org/LDP/abs/html/exitcodes.html - #exit 99 + exit 100 fi diff --git a/code/resources/win32/bin/code.cmd b/code/resources/win32/bin/code.cmd index 9da8ab4f7b8..7e7b92c9eb7 100644 --- a/code/resources/win32/bin/code.cmd +++ b/code/resources/win32/bin/code.cmd @@ -3,4 +3,5 @@ setlocal set VSCODE_DEV= set ELECTRON_RUN_AS_NODE=1 "%~dp0..\@@NAME@@.exe" "%~dp0..\resources\app\out\cli.js" %* +IF %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL% endlocal diff --git a/code/src/vs/base/browser/dom.ts b/code/src/vs/base/browser/dom.ts index 82459f97178..ff113c9baa9 100644 --- a/code/src/vs/base/browser/dom.ts +++ b/code/src/vs/base/browser/dom.ts @@ -921,13 +921,6 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node): void { - const window = getWindow(element); - if (!window.document.hasFocus()) { - window.focus(); - } -} - const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/code/src/vs/base/browser/fonts.ts b/code/src/vs/base/browser/fonts.ts new file mode 100644 index 00000000000..a5e78d00bef --- /dev/null +++ b/code/src/vs/base/browser/fonts.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isMacintosh, isWindows } from 'vs/base/common/platform'; + +/** + * The best font-family to be used in CSS based on the platform: + * - Windows: Segoe preferred, fallback to sans-serif + * - macOS: standard system font, fallback to sans-serif + * - Linux: standard system font preferred, fallback to Ubuntu fonts + * + * Note: this currently does not adjust for different locales. + */ +export const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif'; diff --git a/code/src/vs/base/browser/keyboardEvent.ts b/code/src/vs/base/browser/keyboardEvent.ts index 57ba7407845..6aa5bf530f3 100644 --- a/code/src/vs/base/browser/keyboardEvent.ts +++ b/code/src/vs/base/browser/keyboardEvent.ts @@ -142,7 +142,7 @@ export class StandardKeyboardEvent implements IKeyboardEvent { this.shiftKey = e.shiftKey; this.altKey = e.altKey; this.metaKey = e.metaKey; - this.altGraphKey = e.getModifierState('AltGraph'); + this.altGraphKey = e.getModifierState?.('AltGraph'); this.keyCode = extractKeyCode(e); this.code = e.code; diff --git a/code/src/vs/base/browser/touch.ts b/code/src/vs/base/browser/touch.ts index 79e3211d85f..db93de17f58 100644 --- a/code/src/vs/base/browser/touch.ts +++ b/code/src/vs/base/browser/touch.ts @@ -274,12 +274,25 @@ export class Gesture extends Disposable { } } + const targets: [number, HTMLElement][] = []; for (const target of this.targets) { if (target.contains(event.initialTarget)) { - target.dispatchEvent(event); - this.dispatched = true; + let depth = 0; + let now: Node | null = event.initialTarget; + while (now && now !== target) { + depth++; + now = now.parentElement; + } + targets.push([depth, target]); } } + + targets.sort((a, b) => a[0] - b[0]); + + for (const [_, target] of targets) { + target.dispatchEvent(event); + this.dispatched = true; + } } } diff --git a/code/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/code/src/vs/base/browser/ui/actionbar/actionViewItems.ts index fd7424b0f72..df6ec99660c 100644 --- a/code/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/code/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -9,9 +9,9 @@ import { addDisposableListener, EventHelper, EventLike, EventType } from 'vs/bas import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ISelectBoxOptions, ISelectBoxStyles, ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { Action, ActionRunner, IAction, IActionChangeEvent, IActionRunner, Separator } from 'vs/base/common/actions'; @@ -227,14 +227,13 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { this.updateAriaLabel(); if (this.options.hoverDelegate?.showNativeHover) { - /* While custom hover is not supported with context view */ + /* While custom hover is not inside custom hover */ this.element.title = title; } else { - if (!this.customHover) { + if (!this.customHover && title !== '') { const hoverDelegate = this.options.hoverDelegate ?? getDefaultHoverDelegate('element'); - this.customHover = setupCustomHover(hoverDelegate, this.element, title); - this._store.add(this.customHover); - } else { + this.customHover = this._store.add(setupCustomHover(hoverDelegate, this.element, title)); + } else if (this.customHover) { this.customHover.update(title); } } diff --git a/code/src/vs/base/browser/ui/actionbar/actionbar.ts b/code/src/vs/base/browser/ui/actionbar/actionbar.ts index 5f5ad352842..05505b768a5 100644 --- a/code/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/code/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -6,8 +6,8 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { ActionRunner, IAction, IActionRunner, IRunEvent, Separator } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -118,7 +118,7 @@ export class ActionBar extends Disposable implements IActionRunner { keys: this.options.triggerKeys?.keys ?? [KeyCode.Enter, KeyCode.Space] }; - this._hoverDelegate = options.hoverDelegate ?? this._register(getDefaultHoverDelegate('element', true)); + this._hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate()); if (this.options.actionRunner) { this._actionRunner = this.options.actionRunner; diff --git a/code/src/vs/base/browser/ui/button/button.ts b/code/src/vs/base/browser/ui/button/button.ts index e7ca41dc8b3..1d2a3364d0b 100644 --- a/code/src/vs/base/browser/ui/button/button.ts +++ b/code/src/vs/base/browser/ui/button/button.ts @@ -9,9 +9,9 @@ import { sanitize } from 'vs/base/browser/dompurify/dompurify'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown, renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; @@ -303,9 +303,9 @@ export class Button extends Disposable implements IButton { } private setTitle(title: string) { - if (!this._hover) { + if (!this._hover && title !== '') { this._hover = this._register(setupCustomHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); - } else { + } else if (this._hover) { this._hover.update(title); } } diff --git a/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 373f406613f..25c7f25a69b 100644 Binary files a/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/code/src/vs/base/browser/ui/contextview/contextview.ts b/code/src/vs/base/browser/ui/contextview/contextview.ts index af49847a810..a7debba4e6c 100644 --- a/code/src/vs/base/browser/ui/contextview/contextview.ts +++ b/code/src/vs/base/browser/ui/contextview/contextview.ts @@ -60,6 +60,9 @@ export interface IDelegate { canRelayout?: boolean; // default: true onDOMEvent?(e: Event, activeElement: HTMLElement): void; onHide?(data?: unknown): void; + + // context views with higher layers are rendered over contet views with lower layers + layer?: number; // Default: 0 } export interface IContextViewProvider { @@ -222,7 +225,7 @@ export class ContextView extends Disposable { this.view.className = 'context-view'; this.view.style.top = '0px'; this.view.style.left = '0px'; - this.view.style.zIndex = '2575'; + this.view.style.zIndex = `${2575 + (delegate.layer ?? 0)}`; this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute'; DOM.show(this.view); diff --git a/code/src/vs/base/browser/ui/dropdown/dropdown.ts b/code/src/vs/base/browser/ui/dropdown/dropdown.ts index 88dfaf2c5b1..dfc2329510f 100644 --- a/code/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/code/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -8,8 +8,8 @@ import { $, addDisposableListener, append, EventHelper, EventType, isMouseEvent import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IMenuOptions } from 'vs/base/browser/ui/menu/menu'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; @@ -105,9 +105,9 @@ class BaseDropdown extends ActionRunner { set tooltip(tooltip: string) { if (this._label) { - if (!this.hover) { + if (!this.hover && tooltip !== '') { this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._label, tooltip)); - } else { + } else if (this.hover) { this.hover.update(tooltip); } } diff --git a/code/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/code/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 995b8f40817..75333985372 100644 --- a/code/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/code/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -19,8 +19,8 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./dropdown'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface IKeybindingProvider { (action: IAction): ResolvedKeybinding | undefined; diff --git a/code/src/vs/base/browser/ui/findinput/findInput.ts b/code/src/vs/base/browser/ui/findinput/findInput.ts index c119ad43779..9ecfcbce827 100644 --- a/code/src/vs/base/browser/ui/findinput/findInput.ts +++ b/code/src/vs/base/browser/ui/findinput/findInput.ts @@ -16,7 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import 'vs/css!./findInput'; import * as nls from 'vs/nls'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface IFindInputOptions { @@ -114,7 +114,7 @@ export class FindInput extends Widget { inputBoxStyles: options.inputBoxStyles, })); - const hoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + const hoverDelegate = this._register(createInstantHoverDelegate()); if (this.showCommonFindToggles) { this.regex = this._register(new RegexToggle({ diff --git a/code/src/vs/base/browser/ui/findinput/findInputToggles.ts b/code/src/vs/base/browser/ui/findinput/findInputToggles.ts index 8b3ea12580f..adce009430b 100644 --- a/code/src/vs/base/browser/ui/findinput/findInputToggles.ts +++ b/code/src/vs/base/browser/ui/findinput/findInputToggles.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; import * as nls from 'vs/nls'; diff --git a/code/src/vs/base/browser/ui/findinput/replaceInput.ts b/code/src/vs/base/browser/ui/findinput/replaceInput.ts index 8e20025d757..4dfdf549a3b 100644 --- a/code/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/code/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -16,7 +16,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import 'vs/css!./findInput'; import * as nls from 'vs/nls'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface IReplaceInputOptions { diff --git a/code/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/code/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index c2b41545d79..4847da97d2f 100644 --- a/code/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/code/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -4,7 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Disposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; /** @@ -22,13 +26,15 @@ export interface IHighlightedLabelOptions { * Whether the label supports rendering icons. */ readonly supportIcons?: boolean; + + readonly hoverDelegate?: IHoverDelegate; } /** * A widget which can render a label with substring highlights, often * originating from a filter function like the fuzzy matcher. */ -export class HighlightedLabel { +export class HighlightedLabel extends Disposable { private readonly domNode: HTMLElement; private text: string = ''; @@ -36,13 +42,16 @@ export class HighlightedLabel { private highlights: readonly IHighlight[] = []; private supportIcons: boolean; private didEverRender: boolean = false; + private customHover: ICustomHover | undefined; /** * Create a new {@link HighlightedLabel}. * * @param container The parent container to append to. */ - constructor(container: HTMLElement, options?: IHighlightedLabelOptions) { + constructor(container: HTMLElement, private readonly options?: IHighlightedLabelOptions) { + super(); + this.supportIcons = options?.supportIcons ?? false; this.domNode = dom.append(container, dom.$('span.monaco-highlighted-label')); } @@ -125,10 +134,16 @@ export class HighlightedLabel { dom.reset(this.domNode, ...children); - if (this.title) { + if (this.options?.hoverDelegate?.showNativeHover) { + /* While custom hover is not inside custom hover */ this.domNode.title = this.title; } else { - this.domNode.removeAttribute('title'); + if (!this.customHover && this.title !== '') { + const hoverDelegate = this.options?.hoverDelegate ?? getDefaultHoverDelegate('mouse'); + this.customHover = this._register(setupCustomHover(hoverDelegate, this.domNode, this.title)); + } else if (this.customHover) { + this.customHover.update(this.title); + } } this.didEverRender = true; diff --git a/code/src/vs/base/browser/ui/hover/hoverDelegate.ts b/code/src/vs/base/browser/ui/hover/hoverDelegate.ts index 6d2dfef371a..57f6962ac0e 100644 --- a/code/src/vs/base/browser/ui/hover/hoverDelegate.ts +++ b/code/src/vs/base/browser/ui/hover/hoverDelegate.ts @@ -3,33 +3,66 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IHoverDelegate, IScopedHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { Lazy } from 'vs/base/common/lazy'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IHoverWidget, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IDisposable } from 'vs/base/common/lifecycle'; -const nullHoverDelegateFactory = () => ({ - get delay(): number { return -1; }, - dispose: () => { }, - showHover: () => { return undefined; }, -}); - -let hoverDelegateFactory: (placement: 'mouse' | 'element', enableInstantHover: boolean) => IScopedHoverDelegate = nullHoverDelegateFactory; -const defaultHoverDelegateMouse = new Lazy(() => hoverDelegateFactory('mouse', false)); -const defaultHoverDelegateElement = new Lazy(() => hoverDelegateFactory('element', false)); - -export function setHoverDelegateFactory(hoverDelegateProvider: ((placement: 'mouse' | 'element', enableInstantHover: boolean) => IScopedHoverDelegate)): void { - hoverDelegateFactory = hoverDelegateProvider; +export interface IHoverDelegateTarget extends IDisposable { + readonly targetElements: readonly HTMLElement[]; + x?: number; } -export function getDefaultHoverDelegate(placement: 'mouse' | 'element'): IHoverDelegate; -export function getDefaultHoverDelegate(placement: 'element', enableInstantHover: true): IScopedHoverDelegate; -export function getDefaultHoverDelegate(placement: 'mouse' | 'element', enableInstantHover?: boolean): IHoverDelegate | IScopedHoverDelegate { - if (enableInstantHover) { - // If instant hover is enabled, the consumer is responsible for disposing the hover delegate - return hoverDelegateFactory(placement, true); - } +export interface IHoverDelegateOptions extends IUpdatableHoverOptions { + /** + * The content to display in the primary section of the hover. The type of text determines the + * default `hideOnHover` behavior. + */ + content: IMarkdownString | string | HTMLElement; + /** + * The target for the hover. This determines the position of the hover and it will only be + * hidden when the mouse leaves both the hover and the target. A HTMLElement can be used for + * simple cases and a IHoverDelegateTarget for more complex cases where multiple elements and/or a + * dispose method is required. + */ + target: IHoverDelegateTarget | HTMLElement; + /** + * The container to pass to {@link IContextViewProvider.showContextView} which renders the hover + * in. This is particularly useful for more natural tab focusing behavior, where the hover is + * created as the next tab index after the element being hovered and/or to workaround the + * element's container hiding on `focusout`. + */ + container?: HTMLElement; + /** + * Options that defines where the hover is positioned. + */ + position?: { + /** + * Position of the hover. The default is to show above the target. This option will be ignored + * if there is not enough room to layout the hover in the specified position, unless the + * forcePosition option is set. + */ + hoverPosition?: HoverPosition; + }; + appearance?: { + /** + * Whether to show the hover pointer + */ + showPointer?: boolean; + /** + * Whether to skip the fade in animation, this should be used when hovering from one hover to + * another in the same group so it looks like the hover is moving from one element to the other. + */ + skipFadeInAnimation?: boolean; + }; +} - if (placement === 'element') { - return defaultHoverDelegateElement.value; - } - return defaultHoverDelegateMouse.value; +export interface IHoverDelegate { + showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined; + onDidHideHover?: () => void; + delay: number; + placement?: 'mouse' | 'element'; + showNativeHover?: boolean; // TODO@benibenj remove this, only temp fix for contextviews } + +export interface IScopedHoverDelegate extends IHoverDelegate, IDisposable { } diff --git a/code/src/vs/base/browser/ui/hover/hoverDelegateFactory.ts b/code/src/vs/base/browser/ui/hover/hoverDelegateFactory.ts new file mode 100644 index 00000000000..44628261333 --- /dev/null +++ b/code/src/vs/base/browser/ui/hover/hoverDelegateFactory.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. + *--------------------------------------------------------------------------------------------*/ + +import { IHoverDelegate, IScopedHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { Lazy } from 'vs/base/common/lazy'; + +const nullHoverDelegateFactory = () => ({ + get delay(): number { return -1; }, + dispose: () => { }, + showHover: () => { return undefined; }, +}); + +let hoverDelegateFactory: (placement: 'mouse' | 'element', enableInstantHover: boolean) => IScopedHoverDelegate = nullHoverDelegateFactory; +const defaultHoverDelegateMouse = new Lazy(() => hoverDelegateFactory('mouse', false)); +const defaultHoverDelegateElement = new Lazy(() => hoverDelegateFactory('element', false)); + +export function setHoverDelegateFactory(hoverDelegateProvider: ((placement: 'mouse' | 'element', enableInstantHover: boolean) => IScopedHoverDelegate)): void { + hoverDelegateFactory = hoverDelegateProvider; +} + +export function getDefaultHoverDelegate(placement: 'mouse' | 'element'): IHoverDelegate { + if (placement === 'element') { + return defaultHoverDelegateElement.value; + } + return defaultHoverDelegateMouse.value; +} + +export function createInstantHoverDelegate(): IScopedHoverDelegate { + // Creates a hover delegate with instant hover enabled. + // This hover belongs to the consumer and requires the them to dispose it. + // Instant hover only makes sense for 'element' placement. + return hoverDelegateFactory('element', true); +} diff --git a/code/src/vs/base/browser/ui/hover/hover.css b/code/src/vs/base/browser/ui/hover/hoverWidget.css similarity index 100% rename from code/src/vs/base/browser/ui/hover/hover.css rename to code/src/vs/base/browser/ui/hover/hoverWidget.css diff --git a/code/src/vs/base/browser/ui/hover/hoverWidget.ts b/code/src/vs/base/browser/ui/hover/hoverWidget.ts index dc0af66ff5a..bff397303be 100644 --- a/code/src/vs/base/browser/ui/hover/hoverWidget.ts +++ b/code/src/vs/base/browser/ui/hover/hoverWidget.ts @@ -8,7 +8,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import 'vs/css!./hover'; +import 'vs/css!./hoverWidget'; import { localize } from 'vs/nls'; const $ = dom.$; diff --git a/code/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/code/src/vs/base/browser/ui/hover/updatableHoverWidget.ts similarity index 95% rename from code/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts rename to code/src/vs/base/browser/ui/hover/updatableHoverWidget.ts index bdcdfa7c7da..d36ebab0d95 100644 --- a/code/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts +++ b/code/src/vs/base/browser/ui/hover/updatableHoverWidget.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate'; import { TimeoutTimer } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; @@ -68,6 +68,9 @@ export interface ICustomHover extends IDisposable { update(tooltip: IHoverContent, options?: IUpdatableHoverOptions): void; } +export interface IHoverWidget extends IDisposable { + readonly isDisposed: boolean; +} class UpdatableHoverWidget implements IDisposable { @@ -267,7 +270,14 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); hoverPreparation = toDispose; }; - const focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + + // Do not show hover when focusing an input or textarea + let focusDomEmitter: undefined | IDisposable; + const tagName = htmlElement.tagName.toLowerCase(); + if (tagName !== 'input' && tagName !== 'textarea') { + focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + } + const hover: ICustomHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation @@ -285,7 +295,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); mouseUpEmitter.dispose(); - focusDomEmitter.dispose(); + focusDomEmitter?.dispose(); hideHover(true, true); } }; diff --git a/code/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts b/code/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts deleted file mode 100644 index f0209856dc3..00000000000 --- a/code/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IUpdatableHoverOptions } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IDisposable } from 'vs/base/common/lifecycle'; - -export interface IHoverDelegateTarget extends IDisposable { - readonly targetElements: readonly HTMLElement[]; - x?: number; -} - -export interface IHoverDelegateOptions extends IUpdatableHoverOptions { - /** - * The content to display in the primary section of the hover. The type of text determines the - * default `hideOnHover` behavior. - */ - content: IMarkdownString | string | HTMLElement; - /** - * The target for the hover. This determines the position of the hover and it will only be - * hidden when the mouse leaves both the hover and the target. A HTMLElement can be used for - * simple cases and a IHoverDelegateTarget for more complex cases where multiple elements and/or a - * dispose method is required. - */ - target: IHoverDelegateTarget | HTMLElement; - /** - * The container to pass to {@link IContextViewProvider.showContextView} which renders the hover - * in. This is particularly useful for more natural tab focusing behavior, where the hover is - * created as the next tab index after the element being hovered and/or to workaround the - * element's container hiding on `focusout`. - */ - container?: HTMLElement; - /** - * Options that defines where the hover is positioned. - */ - position?: { - /** - * Position of the hover. The default is to show above the target. This option will be ignored - * if there is not enough room to layout the hover in the specified position, unless the - * forcePosition option is set. - */ - hoverPosition?: HoverPosition; - }; - appearance?: { - /** - * Whether to show the hover pointer - */ - showPointer?: boolean; - /** - * Whether to skip the fade in animation, this should be used when hovering from one hover to - * another in the same group so it looks like the hover is moving from one element to the other. - */ - skipFadeInAnimation?: boolean; - }; -} - -export interface IHoverDelegate { - showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined; - onDidHideHover?: () => void; - delay: number; - placement?: 'mouse' | 'element'; - showNativeHover?: boolean; // TODO@benibenj remove this, only temp fix for contextviews -} - -export interface IScopedHoverDelegate extends IHoverDelegate, IDisposable { } - -export interface IHoverWidget extends IDisposable { - readonly isDisposed: boolean; -} diff --git a/code/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/code/src/vs/base/browser/ui/iconLabel/iconLabel.ts index c9d62a9bc4e..c0e0544a93b 100644 --- a/code/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/code/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -6,13 +6,13 @@ import 'vs/css!./iconlabel'; import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { ITooltipMarkdownString, setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ITooltipMarkdownString, setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IMatch } from 'vs/base/common/filters'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equals } from 'vs/base/common/objects'; import { Range } from 'vs/base/common/range'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface IIconLabelCreationOptions { readonly supportHighlights?: boolean; @@ -109,7 +109,7 @@ export class IconLabel extends Disposable { this.nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container')); if (options?.supportHighlights || options?.supportIcons) { - this.nameNode = new LabelWithHighlights(this.nameContainer, !!options.supportIcons); + this.nameNode = this._register(new LabelWithHighlights(this.nameContainer, !!options.supportIcons)); } else { this.nameNode = new Label(this.nameContainer); } @@ -218,7 +218,7 @@ export class IconLabel extends Disposable { if (!this.descriptionNode) { const descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container')))); if (this.creationOptions?.supportDescriptionHighlights) { - this.descriptionNode = new HighlightedLabel(dom.append(descriptionContainer.element, dom.$('span.label-description')), { supportIcons: !!this.creationOptions.supportIcons }); + this.descriptionNode = this._register(new HighlightedLabel(dom.append(descriptionContainer.element, dom.$('span.label-description')), { supportIcons: !!this.creationOptions.supportIcons })); } else { this.descriptionNode = this._register(new FastLabelNode(dom.append(descriptionContainer.element, dom.$('span.label-description')))); } @@ -291,13 +291,15 @@ function splitMatches(labels: string[], separator: string, matches: readonly IMa }); } -class LabelWithHighlights { +class LabelWithHighlights extends Disposable { private label: string | string[] | undefined = undefined; private singleLabel: HighlightedLabel | undefined = undefined; private options: IIconLabelValueOptions | undefined; - constructor(private container: HTMLElement, private supportIcons: boolean) { } + constructor(private container: HTMLElement, private supportIcons: boolean) { + super(); + } setLabel(label: string | string[], options?: IIconLabelValueOptions): void { if (this.label === label && equals(this.options, options)) { @@ -311,7 +313,7 @@ class LabelWithHighlights { if (!this.singleLabel) { this.container.innerText = ''; this.container.classList.remove('multiple'); - this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), { supportIcons: this.supportIcons }); + this.singleLabel = this._register(new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), { supportIcons: this.supportIcons })); } this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines); @@ -329,7 +331,7 @@ class LabelWithHighlights { const id = options?.domId && `${options?.domId}_${i}`; const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }); - const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), { supportIcons: this.supportIcons }); + const highlightedLabel = this._register(new HighlightedLabel(dom.append(this.container, name), { supportIcons: this.supportIcons })); highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines); if (i < label.length - 1) { diff --git a/code/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts b/code/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts index 659572d4ff8..dc35bd8a9ab 100644 --- a/code/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts +++ b/code/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts @@ -4,9 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { reset } from 'vs/base/browser/dom'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { IDisposable } from 'vs/base/common/lifecycle'; -export class SimpleIconLabel { +export class SimpleIconLabel implements IDisposable { + + private hover?: ICustomHover; constructor( private readonly _container: HTMLElement @@ -17,6 +22,14 @@ export class SimpleIconLabel { } set title(title: string) { - this._container.title = title; + if (!this.hover && title) { + this.hover = setupCustomHover(getDefaultHoverDelegate('mouse'), this._container, title); + } else if (this.hover) { + this.hover.update(title); + } + } + + dispose(): void { + this.hover?.dispose(); } } diff --git a/code/src/vs/base/browser/ui/icons/iconSelectBox.ts b/code/src/vs/base/browser/ui/icons/iconSelectBox.ts index 465c1dc1181..b59529ffd81 100644 --- a/code/src/vs/base/browser/ui/icons/iconSelectBox.ts +++ b/code/src/vs/base/browser/ui/icons/iconSelectBox.ts @@ -81,7 +81,7 @@ export class IconSelectBox extends Disposable { dom.append(iconSelectBoxContainer, this.scrollableElement.getDomNode()); if (this.options.showIconInfo) { - this.iconIdElement = new HighlightedLabel(dom.append(dom.append(iconSelectBoxContainer, dom.$('.icon-select-id-container')), dom.$('.icon-select-id-label'))); + this.iconIdElement = this._register(new HighlightedLabel(dom.append(dom.append(iconSelectBoxContainer, dom.$('.icon-select-id-container')), dom.$('.icon-select-id-label')))); } const iconsDisposables = disposables.add(new MutableDisposable()); diff --git a/code/src/vs/base/browser/ui/inputbox/inputBox.ts b/code/src/vs/base/browser/ui/inputbox/inputBox.ts index e4c89dd3aff..0a06ece485f 100644 --- a/code/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/code/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -11,6 +11,8 @@ import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Widget } from 'vs/base/browser/ui/widget'; import { IAction } from 'vs/base/common/actions'; @@ -111,6 +113,7 @@ export class InputBox extends Widget { private cachedContentHeight: number | undefined; private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; + private hover: ICustomHover | undefined; private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -230,7 +233,11 @@ export class InputBox extends Widget { public setTooltip(tooltip: string): void { this.tooltip = tooltip; - this.input.title = tooltip; + if (!this.hover) { + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); + } else { + this.hover.update(tooltip); + } } public setAriaLabel(label: string): void { diff --git a/code/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts b/code/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts index 431e33048cd..20eea5d8850 100644 --- a/code/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts +++ b/code/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts @@ -4,8 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { UILabelProvider } from 'vs/base/common/keybindingLabels'; import { ResolvedKeybinding, ResolvedChord } from 'vs/base/common/keybindings'; +import { Disposable } from 'vs/base/common/lifecycle'; import { equals } from 'vs/base/common/objects'; import { OperatingSystem } from 'vs/base/common/platform'; import 'vs/css!./keybindingLabel'; @@ -50,18 +53,21 @@ export const unthemedKeybindingLabelOptions: KeybindingLabelOptions = { keybindingLabelShadow: undefined }; -export class KeybindingLabel { +export class KeybindingLabel extends Disposable { private domNode: HTMLElement; private options: KeybindingLabelOptions; private readonly keyElements = new Set(); + private hover: ICustomHover; private keybinding: ResolvedKeybinding | undefined; private matches: Matches | undefined; private didEverRender: boolean; constructor(container: HTMLElement, private os: OperatingSystem, options?: KeybindingLabelOptions) { + super(); + this.options = options || Object.create(null); const labelForeground = this.options.keybindingLabelForeground; @@ -71,6 +77,8 @@ export class KeybindingLabel { this.domNode.style.color = labelForeground; } + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.domNode, '')); + this.didEverRender = false; container.appendChild(this.domNode); } @@ -102,11 +110,8 @@ export class KeybindingLabel { this.renderChord(this.domNode, chords[i], this.matches ? this.matches.chordPart : null); } const title = (this.options.disableTitle ?? false) ? undefined : this.keybinding.getAriaLabel() || undefined; - if (title !== undefined) { - this.domNode.title = title; - } else { - this.domNode.removeAttribute('title'); - } + this.hover.update(title); + this.domNode.setAttribute('aria-label', title || ''); } else if (this.options && this.options.renderUnboundKeybindings) { this.renderUnbound(this.domNode); } diff --git a/code/src/vs/base/browser/ui/menu/menu.ts b/code/src/vs/base/browser/ui/menu/menu.ts index 4429e37687d..187ac848e8f 100644 --- a/code/src/vs/base/browser/ui/menu/menu.ts +++ b/code/src/vs/base/browser/ui/menu/menu.ts @@ -14,7 +14,8 @@ import { AnchorAlignment, layout, LayoutAnchorPosition } from 'vs/base/browser/u import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { EmptySubmenuAction, IAction, IActionRunner, Separator, SubmenuAction } from 'vs/base/common/actions'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { Codicon, getCodiconFontCharacters } from 'vs/base/common/codicons'; +import { Codicon } from 'vs/base/common/codicons'; +import { getCodiconFontCharacters } from 'vs/base/common/codiconsUtil'; import { ThemeIcon } from 'vs/base/common/themables'; import { Event } from 'vs/base/common/event'; import { stripIcons } from 'vs/base/common/iconLabels'; diff --git a/code/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/code/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 83e2d27eef7..be9064254c1 100644 --- a/code/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/code/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -5,7 +5,7 @@ import { getZoomFactor, isChrome } from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; -import { createFastDomNode, FastDomNode } from 'vs/base/browser/fastDomNode'; +import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseEvent, IMouseWheelEvent, StandardWheelEvent } from 'vs/base/browser/mouseEvent'; import { ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; import { HorizontalScrollbar } from 'vs/base/browser/ui/scrollbar/horizontalScrollbar'; @@ -14,9 +14,9 @@ import { VerticalScrollbar } from 'vs/base/browser/ui/scrollbar/verticalScrollba import { Widget } from 'vs/base/browser/ui/widget'; import { TimeoutTimer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; -import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; +import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; import 'vs/css!./media/scrollbars'; const HIDE_TIMEOUT = 500; @@ -99,14 +99,16 @@ export class MouseWheelClassifier { } public accept(timestamp: number, deltaX: number, deltaY: number): void { + let previousItem = null; const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY); - item.score = this._computeScore(item); if (this._front === -1 && this._rear === -1) { this._memory[0] = item; this._front = 0; this._rear = 0; } else { + previousItem = this._memory[this._rear]; + this._rear = (this._rear + 1) % this._capacity; if (this._rear === this._front) { // Drop oldest @@ -114,6 +116,8 @@ export class MouseWheelClassifier { } this._memory[this._rear] = item; } + + item.score = this._computeScore(item, previousItem); } /** @@ -121,7 +125,7 @@ export class MouseWheelClassifier { * - a score towards 0 indicates that the source appears to be a physical mouse wheel * - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc. */ - private _computeScore(item: MouseWheelClassifierItem): number { + private _computeScore(item: MouseWheelClassifierItem, previousItem: MouseWheelClassifierItem | null): number { if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) { // both axes exercised => definitely not a physical mouse wheel @@ -129,25 +133,34 @@ export class MouseWheelClassifier { } let score: number = 0.5; - const prev = (this._front === -1 && this._rear === -1 ? null : this._memory[this._rear]); - if (prev) { - // const deltaT = item.timestamp - prev.timestamp; - // if (deltaT < 1000 / 30) { - // // sooner than X times per second => indicator that this is not a physical mouse wheel - // score += 0.25; - // } - - // if (item.deltaX === prev.deltaX && item.deltaY === prev.deltaY) { - // // equal amplitude => indicator that this is a physical mouse wheel - // score -= 0.25; - // } - } if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) { // non-integer deltas => indicator that this is not a physical mouse wheel score += 0.25; } + // Non-accelerating scroll => indicator that this is a physical mouse wheel + // These can be identified by seeing whether they are the module of one another. + if (previousItem) { + const absDeltaX = Math.abs(item.deltaX); + const absDeltaY = Math.abs(item.deltaY); + + const absPreviousDeltaX = Math.abs(previousItem.deltaX); + const absPreviousDeltaY = Math.abs(previousItem.deltaY); + + // Min 1 to avoid division by zero, module 1 will still be 0. + const minDeltaX = Math.max(Math.min(absDeltaX, absPreviousDeltaX), 1); + const minDeltaY = Math.max(Math.min(absDeltaY, absPreviousDeltaY), 1); + + const maxDeltaX = Math.max(absDeltaX, absPreviousDeltaX); + const maxDeltaY = Math.max(absDeltaY, absPreviousDeltaY); + + const isSameModulo = (maxDeltaX % minDeltaX === 0 && maxDeltaY % minDeltaY === 0); + if (isSameModulo) { + score -= 0.5; + } + } + return Math.min(Math.max(score, 0), 1); } @@ -383,6 +396,7 @@ export abstract class AbstractScrollableElement extends Widget { classifier.acceptStandardWheelEvent(e); } + // useful for creating unit tests: // console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`); let didScroll = false; diff --git a/code/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/code/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index ca6aaa33505..c813532f496 100644 --- a/code/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/code/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -9,8 +9,8 @@ import { IContentActionHandler } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { AnchorPosition, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IListEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { ISelectBoxDelegate, ISelectBoxOptions, ISelectBoxStyles, ISelectData, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; @@ -103,7 +103,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi private selectionDetailsPane!: HTMLElement; private _skipLayout: boolean = false; private _cachedMaxDetailsHeight?: number; - private _hover: ICustomHover; + private _hover?: ICustomHover; private _sticky: boolean = false; // for dev purposes only @@ -134,8 +134,6 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi this.selectElement.setAttribute('aria-description', this.selectBoxOptions.ariaDescription); } - this._hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.selectElement, '')); - this._onDidSelect = new Emitter(); this._register(this._onDidSelect); @@ -152,6 +150,14 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } + private setTitle(title: string): void { + if (!this._hover && title) { + this._hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.selectElement, title)); + } else if (this._hover) { + this._hover.update(title); + } + } + // IDelegate - List renderer getHeight(): number { @@ -204,7 +210,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi selected: e.target.value }); if (!!this.options[this.selected] && !!this.options[this.selected].text) { - this._hover.update(this.options[this.selected].text); + this.setTitle(this.options[this.selected].text); } })); @@ -314,7 +320,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi this.selectElement.selectedIndex = this.selected; if (!!this.options[this.selected] && !!this.options[this.selected].text) { - this._hover.update(this.options[this.selected].text); + this.setTitle(this.options[this.selected].text); } } @@ -842,7 +848,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi }); if (!!this.options[this.selected] && !!this.options[this.selected].text) { - this._hover.update(this.options[this.selected].text); + this.setTitle(this.options[this.selected].text); } } @@ -941,7 +947,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi selected: this.options[this.selected].text }); if (!!this.options[this.selected] && !!this.options[this.selected].text) { - this._hover.update(this.options[this.selected].text); + this.setTitle(this.options[this.selected].text); } } diff --git a/code/src/vs/base/browser/ui/table/tableWidget.ts b/code/src/vs/base/browser/ui/table/tableWidget.ts index 536fb25608e..38192d39413 100644 --- a/code/src/vs/base/browser/ui/table/tableWidget.ts +++ b/code/src/vs/base/browser/ui/table/tableWidget.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { $, append, clearNode, createStyleSheet, getContentHeight, getContentWidth } from 'vs/base/browser/dom'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListOptions, IListOptionsUpdate, IListStyles, List, unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ISplitViewDescriptor, IView, Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview'; @@ -132,7 +132,10 @@ class ColumnHeader extends Disposable implements IView { super(); this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label); - this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip)); + + if (column.tooltip) { + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip)); + } } layout(size: number): void { diff --git a/code/src/vs/base/browser/ui/toggle/toggle.ts b/code/src/vs/base/browser/ui/toggle/toggle.ts index 540bb17db2f..22d60e98508 100644 --- a/code/src/vs/base/browser/ui/toggle/toggle.ts +++ b/code/src/vs/base/browser/ui/toggle/toggle.ts @@ -13,9 +13,9 @@ import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import 'vs/css!./toggle'; import { isActiveElement, $, addDisposableListener, EventType } from 'vs/base/browser/dom'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; export interface IToggleOpts extends IToggleStyles { readonly actionClassName?: string; @@ -59,6 +59,7 @@ export class ToggleActionViewItem extends BaseActionViewItem { inputActiveOptionBackground: options.toggleStyles?.inputActiveOptionBackground, inputActiveOptionBorder: options.toggleStyles?.inputActiveOptionBorder, inputActiveOptionForeground: options.toggleStyles?.inputActiveOptionForeground, + hoverDelegate: options.hoverDelegate })); this._register(this.toggle.onChange(() => this._action.checked = !!this.toggle && this.toggle.checked)); } diff --git a/code/src/vs/base/browser/ui/toolbar/toolbar.ts b/code/src/vs/base/browser/ui/toolbar/toolbar.ts index f92369cddfb..ebb6bc264d1 100644 --- a/code/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/code/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -15,8 +15,8 @@ import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import 'vs/css!./toolbar'; import * as nls from 'vs/nls'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; @@ -60,7 +60,7 @@ export class ToolBar extends Disposable { constructor(container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) { super(); - options.hoverDelegate = options.hoverDelegate ?? this._register(getDefaultHoverDelegate('element', true)); + options.hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate()); this.options = options; this.lookupKeybindings = typeof this.options.getKeyBinding === 'function'; diff --git a/code/src/vs/base/browser/ui/tree/abstractTree.ts b/code/src/vs/base/browser/ui/tree/abstractTree.ts index c42cd0ba336..ed76c729a07 100644 --- a/code/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/code/src/vs/base/browser/ui/tree/abstractTree.ts @@ -33,8 +33,8 @@ import { ISpliceable } from 'vs/base/common/sequence'; import { isNumber } from 'vs/base/common/types'; import 'vs/css!./media/tree'; import { localize } from 'vs/nls'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; class TreeElementsDragAndDropData extends ElementsDragAndDropData { @@ -807,7 +807,7 @@ class FindWidget extends Disposable { this.elements.root.style.boxShadow = `0 0 8px 2px ${styles.listFilterWidgetShadow}`; } - const toggleHoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + const toggleHoverDelegate = this._register(createInstantHoverDelegate()); this.modeToggle = this._register(new ModeToggle({ ...styles.toggleStyles, isChecked: mode === TreeFindMode.Filter, hoverDelegate: toggleHoverDelegate })); this.matchTypeToggle = this._register(new FuzzyToggle({ ...styles.toggleStyles, isChecked: matchType === TreeFindMatchType.Fuzzy, hoverDelegate: toggleHoverDelegate })); this.onDidChangeMode = Event.map(this.modeToggle.onChange, () => this.modeToggle.checked ? TreeFindMode.Filter : TreeFindMode.Highlight, this._store); @@ -1663,8 +1663,14 @@ class StickyScrollWidget implements IDisposable { // Sticky element container const stickyElement = document.createElement('div'); stickyElement.style.top = `${stickyNode.position}px`; - stickyElement.style.height = `${stickyNode.height}px`; - stickyElement.style.lineHeight = `${stickyNode.height}px`; + + if (this.tree.options.setRowHeight !== false) { + stickyElement.style.height = `${stickyNode.height}px`; + } + + if (this.tree.options.setRowLineHeight !== false) { + stickyElement.style.lineHeight = `${stickyNode.height}px`; + } stickyElement.classList.add('monaco-tree-sticky-row'); stickyElement.classList.add('monaco-list-row'); @@ -1974,11 +1980,31 @@ class StickyScrollFocus extends Disposable { } private toggleElementFocus(element: HTMLElement, focused: boolean): void { + this.toggleElementActiveFocus(element, focused && this.domHasFocus); + this.toggleElementPassiveFocus(element, focused); + } + + private toggleCurrentElementActiveFocus(focused: boolean): void { + if (this.focusedIndex === -1) { + return; + } + this.toggleElementActiveFocus(this.elements[this.focusedIndex], focused); + } + + private toggleElementActiveFocus(element: HTMLElement, focused: boolean) { + // active focus is set when sticky scroll has focus element.classList.toggle('focused', focused); } + private toggleElementPassiveFocus(element: HTMLElement, focused: boolean) { + // passive focus allows to show focus when sticky scroll does not have focus + // for example when the context menu has focus + element.classList.toggle('passive-focused', focused); + } + private toggleStickyScrollFocused(focused: boolean) { // Weather the last focus in the view was sticky scroll and not the list + // Is only removed when the focus is back in the tree an no longer in sticky scroll this.view.getHTMLElement().classList.toggle('sticky-scroll-focused', focused); } @@ -1988,6 +2014,7 @@ class StickyScrollFocus extends Disposable { } this.domHasFocus = true; this.toggleStickyScrollFocused(true); + this.toggleCurrentElementActiveFocus(true); if (this.focusedIndex === -1) { this.setFocus(0); } @@ -1995,6 +2022,7 @@ class StickyScrollFocus extends Disposable { private onBlur(): void { this.domHasFocus = false; + this.toggleCurrentElementActiveFocus(false); } override dispose(): void { @@ -2054,6 +2082,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { export interface IAbstractTreeOptions extends IAbstractTreeOptionsUpdate, IListOptions { readonly contextViewProvider?: IContextViewProvider; readonly collapseByDefault?: boolean; // defaults to false + readonly allowNonCollapsibleParents?: boolean; // defaults to false readonly filter?: ITreeFilter; readonly dnd?: ITreeDragAndDrop; readonly paddingBottom?: number; @@ -2438,6 +2467,8 @@ export abstract class AbstractTree implements IDisposable get onMouseClick(): Event> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); } get onMouseDblClick(): Event> { return Event.filter(Event.map(this.view.onMouseDblClick, asTreeMouseEvent), e => e.target !== TreeMouseEventTarget.Filter); } + get onMouseOver(): Event> { return Event.map(this.view.onMouseOver, asTreeMouseEvent); } + get onMouseOut(): Event> { return Event.map(this.view.onMouseOut, asTreeMouseEvent); } get onContextMenu(): Event> { return Event.any(Event.filter(Event.map(this.view.onContextMenu, asTreeContextMenuEvent), e => !e.isStickyScroll), this.stickyScrollController?.onContextMenu ?? Event.None); } get onTap(): Event> { return Event.map(this.view.onTap, asTreeMouseEvent); } get onPointer(): Event> { return Event.map(this.view.onPointer, asTreeMouseEvent); } @@ -2747,6 +2778,8 @@ export abstract class AbstractTree implements IDisposable content.push(`.monaco-list${suffix}.sticky-scroll-focused .monaco-scrollable-element .monaco-tree-sticky-container:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }`); content.push(`.monaco-list${suffix}:not(.sticky-scroll-focused) .monaco-scrollable-element .monaco-tree-sticky-container .monaco-list-row.focused { outline: inherit; }`); + content.push(`.monaco-workbench.context-menu-visible .monaco-list${suffix}.last-focused.sticky-scroll-focused .monaco-scrollable-element .monaco-tree-sticky-container .monaco-list-row.passive-focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }`); + content.push(`.monaco-workbench.context-menu-visible .monaco-list${suffix}.last-focused.sticky-scroll-focused .monaco-list-rows .monaco-list-row.focused { outline: inherit; }`); content.push(`.monaco-workbench.context-menu-visible .monaco-list${suffix}.last-focused:not(.sticky-scroll-focused) .monaco-tree-sticky-container .monaco-list-rows .monaco-list-row.focused { outline: inherit; }`); } @@ -2876,27 +2909,27 @@ export abstract class AbstractTree implements IDisposable }); } - focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusNext(n, loop, browserEvent, filter); } - focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusPrevious(n, loop, browserEvent, filter); } - focusNextPage(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { + focusNextPage(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { return this.view.focusNextPage(browserEvent, filter); } - focusPreviousPage(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { + focusPreviousPage(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { return this.view.focusPreviousPage(browserEvent, filter, () => this.stickyScrollController?.height ?? 0); } - focusLast(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusLast(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusLast(browserEvent, filter); } - focusFirst(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusFirst(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusFirst(browserEvent, filter); } diff --git a/code/src/vs/base/browser/ui/tree/indexTreeModel.ts b/code/src/vs/base/browser/ui/tree/indexTreeModel.ts index 4e83338804b..219b7c143f1 100644 --- a/code/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/code/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -42,6 +42,7 @@ export function getVisibleState(visibility: boolean | TreeVisibility): TreeVisib export interface IIndexTreeModelOptions { readonly collapseByDefault?: boolean; // defaults to false + readonly allowNonCollapsibleParents?: boolean; // defaults to false readonly filter?: ITreeFilter; readonly autoExpandSingleChildren?: boolean; } @@ -107,6 +108,7 @@ export class IndexTreeModel, TFilterData = voi readonly onDidChangeRenderNodeCount: Event> = this.eventBufferer.wrapEvent(this._onDidChangeRenderNodeCount.event); private collapseByDefault: boolean; + private allowNonCollapsibleParents: boolean; private filter?: ITreeFilter; private autoExpandSingleChildren: boolean; @@ -122,6 +124,7 @@ export class IndexTreeModel, TFilterData = voi options: IIndexTreeModelOptions = {} ) { this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault; + this.allowNonCollapsibleParents = options.allowNonCollapsibleParents ?? false; this.filter = options.filter; this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren; @@ -535,7 +538,10 @@ export class IndexTreeModel, TFilterData = voi } } - node.collapsible = node.collapsible || node.children.length > 0; + if (!this.allowNonCollapsibleParents) { + node.collapsible = node.collapsible || node.children.length > 0; + } + node.visibleChildrenCount = visibleChildrenCount; node.visible = visibility === TreeVisibility.Recurse ? visibleChildrenCount > 0 : (visibility === TreeVisibility.Visible); diff --git a/code/src/vs/base/browser/window.ts b/code/src/vs/base/browser/window.ts index fe715d6f2c2..ab920e18349 100644 --- a/code/src/vs/base/browser/window.ts +++ b/code/src/vs/base/browser/window.ts @@ -20,13 +20,6 @@ export function ensureCodeWindow(targetWindow: Window, fallbackWindowId: number) // eslint-disable-next-line no-restricted-globals export const mainWindow = window as CodeWindow; -/** - * @deprecated to support multi-window scenarios, use `DOM.mainWindow` - * if you target the main global window or use helpers such as `DOM.getWindow()` - * or `DOM.getActiveWindow()` to obtain the correct window for the context you are in. - */ -export const $window = mainWindow; - export function isAuxiliaryWindow(obj: Window): obj is CodeWindow { if (obj === mainWindow) { return false; diff --git a/code/src/vs/base/common/arrays.ts b/code/src/vs/base/common/arrays.ts index 9b510f82251..f8804af0fe7 100644 --- a/code/src/vs/base/common/arrays.ts +++ b/code/src/vs/base/common/arrays.ts @@ -859,3 +859,36 @@ export class CallbackIterable { return result; } } + +/** + * Represents a re-arrangement of items in an array. + */ +export class Permutation { + constructor(private readonly _indexMap: readonly number[]) { } + + /** + * Returns a permutation that sorts the given array according to the given compare function. + */ + public static createSortPermutation(arr: readonly T[], compareFn: (a: T, b: T) => number): Permutation { + const sortIndices = Array.from(arr.keys()).sort((index1, index2) => compareFn(arr[index1], arr[index2])); + return new Permutation(sortIndices); + } + + /** + * Returns a new array with the elements of the given array re-arranged according to this permutation. + */ + apply(arr: readonly T[]): T[] { + return arr.map((_, index) => arr[this._indexMap[index]]); + } + + /** + * Returns a new permutation that undoes the re-arrangement of this permutation. + */ + inverse(): Permutation { + const inverseIndexMap = this._indexMap.slice(); + for (let i = 0; i < this._indexMap.length; i++) { + inverseIndexMap[this._indexMap[i]] = i; + } + return new Permutation(inverseIndexMap); + } +} diff --git a/code/src/vs/base/common/codicons.ts b/code/src/vs/base/common/codicons.ts index 27423d734fd..6919e4934a2 100644 --- a/code/src/vs/base/common/codicons.ts +++ b/code/src/vs/base/common/codicons.ts @@ -3,28 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ThemeIcon } from 'vs/base/common/themables'; -import { isString } from 'vs/base/common/types'; +import { register } from 'vs/base/common/codiconsUtil'; +import { codiconsLibrary } from 'vs/base/common/codiconsLibrary'; -const _codiconFontCharacters: { [id: string]: number } = Object.create(null); - -function register(id: string, fontCharacter: number | string): ThemeIcon { - if (isString(fontCharacter)) { - const val = _codiconFontCharacters[fontCharacter]; - if (val === undefined) { - throw new Error(`${id} references an unknown codicon: ${fontCharacter}`); - } - fontCharacter = val; - } - _codiconFontCharacters[id] = fontCharacter; - return { id }; -} - -/** - * Only to be used by the iconRegistry. - */ -export function getCodiconFontCharacters(): { [id: string]: number } { - return _codiconFontCharacters; -} /** * Only to be used by the iconRegistry. @@ -34,598 +15,50 @@ export function getAllCodicons(): ThemeIcon[] { } /** - * The Codicon library is a set of default icons that are built-in in VS Code. - * - * In the product (outside of base) Codicons should only be used as defaults. In order to have all icons in VS Code - * themeable, component should define new, UI component specific icons using `iconRegistry.registerIcon`. - * In that call a Codicon can be named as default. + * Derived icons, that could become separate icons. + * These mappings should be moved into the mapping file in the vscode-codicons repo at some point. */ -export const Codicon = { - - // built-in icons, with image name - add: register('add', 0xea60), - plus: register('plus', 0xea60), - gistNew: register('gist-new', 0xea60), - repoCreate: register('repo-create', 0xea60), - lightbulb: register('lightbulb', 0xea61), - lightBulb: register('light-bulb', 0xea61), - repo: register('repo', 0xea62), - repoDelete: register('repo-delete', 0xea62), - gistFork: register('gist-fork', 0xea63), - repoForked: register('repo-forked', 0xea63), - gitPullRequest: register('git-pull-request', 0xea64), - gitPullRequestAbandoned: register('git-pull-request-abandoned', 0xea64), - recordKeys: register('record-keys', 0xea65), - keyboard: register('keyboard', 0xea65), - tag: register('tag', 0xea66), - tagAdd: register('tag-add', 0xea66), - tagRemove: register('tag-remove', 0xea66), - gitPullRequestLabel: register('git-pull-request-label', 0xea66), - person: register('person', 0xea67), - personFollow: register('person-follow', 0xea67), - personOutline: register('person-outline', 0xea67), - personFilled: register('person-filled', 0xea67), - gitBranch: register('git-branch', 0xea68), - gitBranchCreate: register('git-branch-create', 0xea68), - gitBranchDelete: register('git-branch-delete', 0xea68), - sourceControl: register('source-control', 0xea68), - mirror: register('mirror', 0xea69), - mirrorPublic: register('mirror-public', 0xea69), - star: register('star', 0xea6a), - starAdd: register('star-add', 0xea6a), - starDelete: register('star-delete', 0xea6a), - starEmpty: register('star-empty', 0xea6a), - comment: register('comment', 0xea6b), - commentAdd: register('comment-add', 0xea6b), - alert: register('alert', 0xea6c), - warning: register('warning', 0xea6c), - search: register('search', 0xea6d), - searchSave: register('search-save', 0xea6d), - logOut: register('log-out', 0xea6e), - signOut: register('sign-out', 0xea6e), - logIn: register('log-in', 0xea6f), - signIn: register('sign-in', 0xea6f), - eye: register('eye', 0xea70), - eyeUnwatch: register('eye-unwatch', 0xea70), - eyeWatch: register('eye-watch', 0xea70), - circleFilled: register('circle-filled', 0xea71), - primitiveDot: register('primitive-dot', 0xea71), - closeDirty: register('close-dirty', 0xea71), - debugBreakpoint: register('debug-breakpoint', 0xea71), - debugBreakpointDisabled: register('debug-breakpoint-disabled', 0xea71), - debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), - debugHint: register('debug-hint', 0xea71), - primitiveSquare: register('primitive-square', 0xea72), - edit: register('edit', 0xea73), - pencil: register('pencil', 0xea73), - info: register('info', 0xea74), - issueOpened: register('issue-opened', 0xea74), - gistPrivate: register('gist-private', 0xea75), - gitForkPrivate: register('git-fork-private', 0xea75), - lock: register('lock', 0xea75), - mirrorPrivate: register('mirror-private', 0xea75), - close: register('close', 0xea76), - removeClose: register('remove-close', 0xea76), - x: register('x', 0xea76), - repoSync: register('repo-sync', 0xea77), - sync: register('sync', 0xea77), - clone: register('clone', 0xea78), - desktopDownload: register('desktop-download', 0xea78), - beaker: register('beaker', 0xea79), - microscope: register('microscope', 0xea79), - vm: register('vm', 0xea7a), - deviceDesktop: register('device-desktop', 0xea7a), - file: register('file', 0xea7b), - fileText: register('file-text', 0xea7b), - more: register('more', 0xea7c), - ellipsis: register('ellipsis', 0xea7c), - kebabHorizontal: register('kebab-horizontal', 0xea7c), - mailReply: register('mail-reply', 0xea7d), - reply: register('reply', 0xea7d), - organization: register('organization', 0xea7e), - organizationFilled: register('organization-filled', 0xea7e), - organizationOutline: register('organization-outline', 0xea7e), - newFile: register('new-file', 0xea7f), - fileAdd: register('file-add', 0xea7f), - newFolder: register('new-folder', 0xea80), - fileDirectoryCreate: register('file-directory-create', 0xea80), - trash: register('trash', 0xea81), - trashcan: register('trashcan', 0xea81), - history: register('history', 0xea82), - clock: register('clock', 0xea82), - folder: register('folder', 0xea83), - fileDirectory: register('file-directory', 0xea83), - symbolFolder: register('symbol-folder', 0xea83), - logoGithub: register('logo-github', 0xea84), - markGithub: register('mark-github', 0xea84), - github: register('github', 0xea84), - terminal: register('terminal', 0xea85), - console: register('console', 0xea85), - repl: register('repl', 0xea85), - zap: register('zap', 0xea86), - symbolEvent: register('symbol-event', 0xea86), - error: register('error', 0xea87), - stop: register('stop', 0xea87), - variable: register('variable', 0xea88), - symbolVariable: register('symbol-variable', 0xea88), - array: register('array', 0xea8a), - symbolArray: register('symbol-array', 0xea8a), - symbolModule: register('symbol-module', 0xea8b), - symbolPackage: register('symbol-package', 0xea8b), - symbolNamespace: register('symbol-namespace', 0xea8b), - symbolObject: register('symbol-object', 0xea8b), - symbolMethod: register('symbol-method', 0xea8c), - symbolFunction: register('symbol-function', 0xea8c), - symbolConstructor: register('symbol-constructor', 0xea8c), - symbolBoolean: register('symbol-boolean', 0xea8f), - symbolNull: register('symbol-null', 0xea8f), - symbolNumeric: register('symbol-numeric', 0xea90), - symbolNumber: register('symbol-number', 0xea90), - symbolStructure: register('symbol-structure', 0xea91), - symbolStruct: register('symbol-struct', 0xea91), - symbolParameter: register('symbol-parameter', 0xea92), - symbolTypeParameter: register('symbol-type-parameter', 0xea92), - symbolKey: register('symbol-key', 0xea93), - symbolText: register('symbol-text', 0xea93), - symbolReference: register('symbol-reference', 0xea94), - goToFile: register('go-to-file', 0xea94), - symbolEnum: register('symbol-enum', 0xea95), - symbolValue: register('symbol-value', 0xea95), - symbolRuler: register('symbol-ruler', 0xea96), - symbolUnit: register('symbol-unit', 0xea96), - activateBreakpoints: register('activate-breakpoints', 0xea97), - archive: register('archive', 0xea98), - arrowBoth: register('arrow-both', 0xea99), - arrowDown: register('arrow-down', 0xea9a), - arrowLeft: register('arrow-left', 0xea9b), - arrowRight: register('arrow-right', 0xea9c), - arrowSmallDown: register('arrow-small-down', 0xea9d), - arrowSmallLeft: register('arrow-small-left', 0xea9e), - arrowSmallRight: register('arrow-small-right', 0xea9f), - arrowSmallUp: register('arrow-small-up', 0xeaa0), - arrowUp: register('arrow-up', 0xeaa1), - bell: register('bell', 0xeaa2), - bold: register('bold', 0xeaa3), - book: register('book', 0xeaa4), - bookmark: register('bookmark', 0xeaa5), - debugBreakpointConditionalUnverified: register('debug-breakpoint-conditional-unverified', 0xeaa6), - debugBreakpointConditional: register('debug-breakpoint-conditional', 0xeaa7), - debugBreakpointConditionalDisabled: register('debug-breakpoint-conditional-disabled', 0xeaa7), - debugBreakpointDataUnverified: register('debug-breakpoint-data-unverified', 0xeaa8), - debugBreakpointData: register('debug-breakpoint-data', 0xeaa9), - debugBreakpointDataDisabled: register('debug-breakpoint-data-disabled', 0xeaa9), - debugBreakpointLogUnverified: register('debug-breakpoint-log-unverified', 0xeaaa), - debugBreakpointLog: register('debug-breakpoint-log', 0xeaab), - debugBreakpointLogDisabled: register('debug-breakpoint-log-disabled', 0xeaab), - briefcase: register('briefcase', 0xeaac), - broadcast: register('broadcast', 0xeaad), - browser: register('browser', 0xeaae), - bug: register('bug', 0xeaaf), - calendar: register('calendar', 0xeab0), - caseSensitive: register('case-sensitive', 0xeab1), - check: register('check', 0xeab2), - checklist: register('checklist', 0xeab3), - chevronDown: register('chevron-down', 0xeab4), - dropDownButton: register('drop-down-button', 0xeab4), - chevronLeft: register('chevron-left', 0xeab5), - chevronRight: register('chevron-right', 0xeab6), - chevronUp: register('chevron-up', 0xeab7), - chromeClose: register('chrome-close', 0xeab8), - chromeMaximize: register('chrome-maximize', 0xeab9), - chromeMinimize: register('chrome-minimize', 0xeaba), - chromeRestore: register('chrome-restore', 0xeabb), - circle: register('circle', 0xeabc), - circleOutline: register('circle-outline', 0xeabc), - debugBreakpointUnverified: register('debug-breakpoint-unverified', 0xeabc), - circleSlash: register('circle-slash', 0xeabd), - circuitBoard: register('circuit-board', 0xeabe), - clearAll: register('clear-all', 0xeabf), - clippy: register('clippy', 0xeac0), - closeAll: register('close-all', 0xeac1), - cloudDownload: register('cloud-download', 0xeac2), - cloudUpload: register('cloud-upload', 0xeac3), - code: register('code', 0xeac4), - collapseAll: register('collapse-all', 0xeac5), - colorMode: register('color-mode', 0xeac6), - commentDiscussion: register('comment-discussion', 0xeac7), - compareChanges: register('compare-changes', 0xeafd), - creditCard: register('credit-card', 0xeac9), - dash: register('dash', 0xeacc), - dashboard: register('dashboard', 0xeacd), - database: register('database', 0xeace), - debugContinue: register('debug-continue', 0xeacf), - debugDisconnect: register('debug-disconnect', 0xead0), - debugPause: register('debug-pause', 0xead1), - debugRestart: register('debug-restart', 0xead2), - debugStart: register('debug-start', 0xead3), - debugStepInto: register('debug-step-into', 0xead4), - debugStepOut: register('debug-step-out', 0xead5), - debugStepOver: register('debug-step-over', 0xead6), - debugStop: register('debug-stop', 0xead7), - debug: register('debug', 0xead8), - deviceCameraVideo: register('device-camera-video', 0xead9), - deviceCamera: register('device-camera', 0xeada), - deviceMobile: register('device-mobile', 0xeadb), - diffAdded: register('diff-added', 0xeadc), - diffIgnored: register('diff-ignored', 0xeadd), - diffModified: register('diff-modified', 0xeade), - diffRemoved: register('diff-removed', 0xeadf), - diffRenamed: register('diff-renamed', 0xeae0), - diff: register('diff', 0xeae1), - discard: register('discard', 0xeae2), - editorLayout: register('editor-layout', 0xeae3), - emptyWindow: register('empty-window', 0xeae4), - exclude: register('exclude', 0xeae5), - extensions: register('extensions', 0xeae6), - eyeClosed: register('eye-closed', 0xeae7), - fileBinary: register('file-binary', 0xeae8), - fileCode: register('file-code', 0xeae9), - fileMedia: register('file-media', 0xeaea), - filePdf: register('file-pdf', 0xeaeb), - fileSubmodule: register('file-submodule', 0xeaec), - fileSymlinkDirectory: register('file-symlink-directory', 0xeaed), - fileSymlinkFile: register('file-symlink-file', 0xeaee), - fileZip: register('file-zip', 0xeaef), - files: register('files', 0xeaf0), - filter: register('filter', 0xeaf1), - flame: register('flame', 0xeaf2), - foldDown: register('fold-down', 0xeaf3), - foldUp: register('fold-up', 0xeaf4), - fold: register('fold', 0xeaf5), - folderActive: register('folder-active', 0xeaf6), - folderOpened: register('folder-opened', 0xeaf7), - gear: register('gear', 0xeaf8), - gift: register('gift', 0xeaf9), - gistSecret: register('gist-secret', 0xeafa), - gist: register('gist', 0xeafb), - gitCommit: register('git-commit', 0xeafc), - gitCompare: register('git-compare', 0xeafd), - gitMerge: register('git-merge', 0xeafe), - githubAction: register('github-action', 0xeaff), - githubAlt: register('github-alt', 0xeb00), - globe: register('globe', 0xeb01), - grabber: register('grabber', 0xeb02), - graph: register('graph', 0xeb03), - gripper: register('gripper', 0xeb04), - heart: register('heart', 0xeb05), - home: register('home', 0xeb06), - horizontalRule: register('horizontal-rule', 0xeb07), - hubot: register('hubot', 0xeb08), - inbox: register('inbox', 0xeb09), - issueClosed: register('issue-closed', 0xeba4), - issueReopened: register('issue-reopened', 0xeb0b), - issues: register('issues', 0xeb0c), - italic: register('italic', 0xeb0d), - jersey: register('jersey', 0xeb0e), - json: register('json', 0xeb0f), - bracket: register('bracket', 0xeb0f), - kebabVertical: register('kebab-vertical', 0xeb10), - key: register('key', 0xeb11), - law: register('law', 0xeb12), - lightbulbAutofix: register('lightbulb-autofix', 0xeb13), - linkExternal: register('link-external', 0xeb14), - link: register('link', 0xeb15), - listOrdered: register('list-ordered', 0xeb16), - listUnordered: register('list-unordered', 0xeb17), - liveShare: register('live-share', 0xeb18), - loading: register('loading', 0xeb19), - location: register('location', 0xeb1a), - mailRead: register('mail-read', 0xeb1b), - mail: register('mail', 0xeb1c), - markdown: register('markdown', 0xeb1d), - megaphone: register('megaphone', 0xeb1e), - mention: register('mention', 0xeb1f), - milestone: register('milestone', 0xeb20), - gitPullRequestMilestone: register('git-pull-request-milestone', 0xeb20), - mortarBoard: register('mortar-board', 0xeb21), - move: register('move', 0xeb22), - multipleWindows: register('multiple-windows', 0xeb23), - mute: register('mute', 0xeb24), - noNewline: register('no-newline', 0xeb25), - note: register('note', 0xeb26), - octoface: register('octoface', 0xeb27), - openPreview: register('open-preview', 0xeb28), - package: register('package', 0xeb29), - paintcan: register('paintcan', 0xeb2a), - pin: register('pin', 0xeb2b), - play: register('play', 0xeb2c), - run: register('run', 0xeb2c), - plug: register('plug', 0xeb2d), - preserveCase: register('preserve-case', 0xeb2e), - preview: register('preview', 0xeb2f), - project: register('project', 0xeb30), - pulse: register('pulse', 0xeb31), - question: register('question', 0xeb32), - quote: register('quote', 0xeb33), - radioTower: register('radio-tower', 0xeb34), - reactions: register('reactions', 0xeb35), - references: register('references', 0xeb36), - refresh: register('refresh', 0xeb37), - regex: register('regex', 0xeb38), - remoteExplorer: register('remote-explorer', 0xeb39), - remote: register('remote', 0xeb3a), - remove: register('remove', 0xeb3b), - replaceAll: register('replace-all', 0xeb3c), - replace: register('replace', 0xeb3d), - repoClone: register('repo-clone', 0xeb3e), - repoForcePush: register('repo-force-push', 0xeb3f), - repoPull: register('repo-pull', 0xeb40), - repoPush: register('repo-push', 0xeb41), - report: register('report', 0xeb42), - requestChanges: register('request-changes', 0xeb43), - rocket: register('rocket', 0xeb44), - rootFolderOpened: register('root-folder-opened', 0xeb45), - rootFolder: register('root-folder', 0xeb46), - rss: register('rss', 0xeb47), - ruby: register('ruby', 0xeb48), - saveAll: register('save-all', 0xeb49), - saveAs: register('save-as', 0xeb4a), - save: register('save', 0xeb4b), - screenFull: register('screen-full', 0xeb4c), - screenNormal: register('screen-normal', 0xeb4d), - searchStop: register('search-stop', 0xeb4e), - server: register('server', 0xeb50), - settingsGear: register('settings-gear', 0xeb51), - settings: register('settings', 0xeb52), - shield: register('shield', 0xeb53), - smiley: register('smiley', 0xeb54), - sortPrecedence: register('sort-precedence', 0xeb55), - splitHorizontal: register('split-horizontal', 0xeb56), - splitVertical: register('split-vertical', 0xeb57), - squirrel: register('squirrel', 0xeb58), - starFull: register('star-full', 0xeb59), - starHalf: register('star-half', 0xeb5a), - symbolClass: register('symbol-class', 0xeb5b), - symbolColor: register('symbol-color', 0xeb5c), - symbolCustomColor: register('symbol-customcolor', 0xeb5c), - symbolConstant: register('symbol-constant', 0xeb5d), - symbolEnumMember: register('symbol-enum-member', 0xeb5e), - symbolField: register('symbol-field', 0xeb5f), - symbolFile: register('symbol-file', 0xeb60), - symbolInterface: register('symbol-interface', 0xeb61), - symbolKeyword: register('symbol-keyword', 0xeb62), - symbolMisc: register('symbol-misc', 0xeb63), - symbolOperator: register('symbol-operator', 0xeb64), - symbolProperty: register('symbol-property', 0xeb65), - wrench: register('wrench', 0xeb65), - wrenchSubaction: register('wrench-subaction', 0xeb65), - symbolSnippet: register('symbol-snippet', 0xeb66), - tasklist: register('tasklist', 0xeb67), - telescope: register('telescope', 0xeb68), - textSize: register('text-size', 0xeb69), - threeBars: register('three-bars', 0xeb6a), - thumbsdown: register('thumbsdown', 0xeb6b), - thumbsup: register('thumbsup', 0xeb6c), - tools: register('tools', 0xeb6d), - triangleDown: register('triangle-down', 0xeb6e), - triangleLeft: register('triangle-left', 0xeb6f), - triangleRight: register('triangle-right', 0xeb70), - triangleUp: register('triangle-up', 0xeb71), - twitter: register('twitter', 0xeb72), - unfold: register('unfold', 0xeb73), - unlock: register('unlock', 0xeb74), - unmute: register('unmute', 0xeb75), - unverified: register('unverified', 0xeb76), - verified: register('verified', 0xeb77), - versions: register('versions', 0xeb78), - vmActive: register('vm-active', 0xeb79), - vmOutline: register('vm-outline', 0xeb7a), - vmRunning: register('vm-running', 0xeb7b), - watch: register('watch', 0xeb7c), - whitespace: register('whitespace', 0xeb7d), - wholeWord: register('whole-word', 0xeb7e), - window: register('window', 0xeb7f), - wordWrap: register('word-wrap', 0xeb80), - zoomIn: register('zoom-in', 0xeb81), - zoomOut: register('zoom-out', 0xeb82), - listFilter: register('list-filter', 0xeb83), - listFlat: register('list-flat', 0xeb84), - listSelection: register('list-selection', 0xeb85), - selection: register('selection', 0xeb85), - listTree: register('list-tree', 0xeb86), - debugBreakpointFunctionUnverified: register('debug-breakpoint-function-unverified', 0xeb87), - debugBreakpointFunction: register('debug-breakpoint-function', 0xeb88), - debugBreakpointFunctionDisabled: register('debug-breakpoint-function-disabled', 0xeb88), - debugStackframeActive: register('debug-stackframe-active', 0xeb89), - circleSmallFilled: register('circle-small-filled', 0xeb8a), - debugStackframeDot: register('debug-stackframe-dot', 0xeb8a), - debugStackframe: register('debug-stackframe', 0xeb8b), - debugStackframeFocused: register('debug-stackframe-focused', 0xeb8b), - debugBreakpointUnsupported: register('debug-breakpoint-unsupported', 0xeb8c), - symbolString: register('symbol-string', 0xeb8d), - debugReverseContinue: register('debug-reverse-continue', 0xeb8e), - debugStepBack: register('debug-step-back', 0xeb8f), - debugRestartFrame: register('debug-restart-frame', 0xeb90), - callIncoming: register('call-incoming', 0xeb92), - callOutgoing: register('call-outgoing', 0xeb93), - menu: register('menu', 0xeb94), - expandAll: register('expand-all', 0xeb95), - feedback: register('feedback', 0xeb96), - gitPullRequestReviewer: register('git-pull-request-reviewer', 0xeb96), - groupByRefType: register('group-by-ref-type', 0xeb97), - ungroupByRefType: register('ungroup-by-ref-type', 0xeb98), - account: register('account', 0xeb99), - gitPullRequestAssignee: register('git-pull-request-assignee', 0xeb99), - bellDot: register('bell-dot', 0xeb9a), - debugConsole: register('debug-console', 0xeb9b), - library: register('library', 0xeb9c), - output: register('output', 0xeb9d), - runAll: register('run-all', 0xeb9e), - syncIgnored: register('sync-ignored', 0xeb9f), - pinned: register('pinned', 0xeba0), - githubInverted: register('github-inverted', 0xeba1), - debugAlt: register('debug-alt', 0xeb91), - serverProcess: register('server-process', 0xeba2), - serverEnvironment: register('server-environment', 0xeba3), - pass: register('pass', 0xeba4), - stopCircle: register('stop-circle', 0xeba5), - playCircle: register('play-circle', 0xeba6), - record: register('record', 0xeba7), - debugAltSmall: register('debug-alt-small', 0xeba8), - vmConnect: register('vm-connect', 0xeba9), - cloud: register('cloud', 0xebaa), - merge: register('merge', 0xebab), - exportIcon: register('export', 0xebac), - graphLeft: register('graph-left', 0xebad), - magnet: register('magnet', 0xebae), - notebook: register('notebook', 0xebaf), - redo: register('redo', 0xebb0), - checkAll: register('check-all', 0xebb1), - pinnedDirty: register('pinned-dirty', 0xebb2), - passFilled: register('pass-filled', 0xebb3), - circleLargeFilled: register('circle-large-filled', 0xebb4), - circleLarge: register('circle-large', 0xebb5), - circleLargeOutline: register('circle-large-outline', 0xebb5), - combine: register('combine', 0xebb6), - gather: register('gather', 0xebb6), - table: register('table', 0xebb7), - variableGroup: register('variable-group', 0xebb8), - typeHierarchy: register('type-hierarchy', 0xebb9), - typeHierarchySub: register('type-hierarchy-sub', 0xebba), - typeHierarchySuper: register('type-hierarchy-super', 0xebbb), - gitPullRequestCreate: register('git-pull-request-create', 0xebbc), - runAbove: register('run-above', 0xebbd), - runBelow: register('run-below', 0xebbe), - notebookTemplate: register('notebook-template', 0xebbf), - debugRerun: register('debug-rerun', 0xebc0), - workspaceTrusted: register('workspace-trusted', 0xebc1), - workspaceUntrusted: register('workspace-untrusted', 0xebc2), - workspaceUnspecified: register('workspace-unspecified', 0xebc3), - terminalCmd: register('terminal-cmd', 0xebc4), - terminalDebian: register('terminal-debian', 0xebc5), - terminalLinux: register('terminal-linux', 0xebc6), - terminalPowershell: register('terminal-powershell', 0xebc7), - terminalTmux: register('terminal-tmux', 0xebc8), - terminalUbuntu: register('terminal-ubuntu', 0xebc9), - terminalBash: register('terminal-bash', 0xebca), - arrowSwap: register('arrow-swap', 0xebcb), - copy: register('copy', 0xebcc), - personAdd: register('person-add', 0xebcd), - filterFilled: register('filter-filled', 0xebce), - wand: register('wand', 0xebcf), - debugLineByLine: register('debug-line-by-line', 0xebd0), - inspect: register('inspect', 0xebd1), - layers: register('layers', 0xebd2), - layersDot: register('layers-dot', 0xebd3), - layersActive: register('layers-active', 0xebd4), - compass: register('compass', 0xebd5), - compassDot: register('compass-dot', 0xebd6), - compassActive: register('compass-active', 0xebd7), - azure: register('azure', 0xebd8), - issueDraft: register('issue-draft', 0xebd9), - gitPullRequestClosed: register('git-pull-request-closed', 0xebda), - gitPullRequestDraft: register('git-pull-request-draft', 0xebdb), - debugAll: register('debug-all', 0xebdc), - debugCoverage: register('debug-coverage', 0xebdd), - runErrors: register('run-errors', 0xebde), - folderLibrary: register('folder-library', 0xebdf), - debugContinueSmall: register('debug-continue-small', 0xebe0), - beakerStop: register('beaker-stop', 0xebe1), - graphLine: register('graph-line', 0xebe2), - graphScatter: register('graph-scatter', 0xebe3), - pieChart: register('pie-chart', 0xebe4), - bracketDot: register('bracket-dot', 0xebe5), - bracketError: register('bracket-error', 0xebe6), - lockSmall: register('lock-small', 0xebe7), - azureDevops: register('azure-devops', 0xebe8), - verifiedFilled: register('verified-filled', 0xebe9), - newLine: register('newline', 0xebea), - layout: register('layout', 0xebeb), - layoutActivitybarLeft: register('layout-activitybar-left', 0xebec), - layoutActivitybarRight: register('layout-activitybar-right', 0xebed), - layoutPanelLeft: register('layout-panel-left', 0xebee), - layoutPanelCenter: register('layout-panel-center', 0xebef), - layoutPanelJustify: register('layout-panel-justify', 0xebf0), - layoutPanelRight: register('layout-panel-right', 0xebf1), - layoutPanel: register('layout-panel', 0xebf2), - layoutSidebarLeft: register('layout-sidebar-left', 0xebf3), - layoutSidebarRight: register('layout-sidebar-right', 0xebf4), - layoutStatusbar: register('layout-statusbar', 0xebf5), - layoutMenubar: register('layout-menubar', 0xebf6), - layoutCentered: register('layout-centered', 0xebf7), - layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), - layoutPanelOff: register('layout-panel-off', 0xec01), - layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), - target: register('target', 0xebf8), - indent: register('indent', 0xebf9), - recordSmall: register('record-small', 0xebfa), - errorSmall: register('error-small', 0xebfb), - arrowCircleDown: register('arrow-circle-down', 0xebfc), - arrowCircleLeft: register('arrow-circle-left', 0xebfd), - arrowCircleRight: register('arrow-circle-right', 0xebfe), - arrowCircleUp: register('arrow-circle-up', 0xebff), - heartFilled: register('heart-filled', 0xec04), - map: register('map', 0xec05), - mapFilled: register('map-filled', 0xec06), - circleSmall: register('circle-small', 0xec07), - bellSlash: register('bell-slash', 0xec08), - bellSlashDot: register('bell-slash-dot', 0xec09), - commentUnresolved: register('comment-unresolved', 0xec0a), - gitPullRequestGoToChanges: register('git-pull-request-go-to-changes', 0xec0b), - gitPullRequestNewChanges: register('git-pull-request-new-changes', 0xec0c), - searchFuzzy: register('search-fuzzy', 0xec0d), - commentDraft: register('comment-draft', 0xec0e), - send: register('send', 0xec0f), - sparkle: register('sparkle', 0xec10), - insert: register('insert', 0xec11), - mic: register('mic', 0xec12), - thumbsDownFilled: register('thumbsdown-filled', 0xec13), - thumbsUpFilled: register('thumbsup-filled', 0xec14), - coffee: register('coffee', 0xec15), - snake: register('snake', 0xec16), - game: register('game', 0xec17), - vr: register('vr', 0xec18), - chip: register('chip', 0xec19), - piano: register('piano', 0xec1a), - music: register('music', 0xec1b), - micFilled: register('mic-filled', 0xec1c), - gitFetch: register('git-fetch', 0xec1d), - copilot: register('copilot', 0xec1e), - lightbulbSparkle: register('lightbulb-sparkle', 0xec1f), - lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), - robot: register('robot', 0xec20), - sparkleFilled: register('sparkle-filled', 0xec21), - diffSingle: register('diff-single', 0xec22), - diffMultiple: register('diff-multiple', 0xec23), - surroundWith: register('surround-with', 0xec24), - gitStash: register('git-stash', 0xec26), - gitStashApply: register('git-stash-apply', 0xec27), - gitStashPop: register('git-stash-pop', 0xec28), - runAllCoverage: register('run-all-coverage', 0xec2d), - runCoverage: register('run-all-coverage', 0xec2c), - coverage: register('coverage', 0xec2e), - githubProject: register('github-project', 0xec2f), - - // derived icons, that could become separate icons - // TODO: These mappings should go in the vscode-codicons mapping file - +export const codiconsDerived = { dialogError: register('dialog-error', 'error'), dialogWarning: register('dialog-warning', 'warning'), dialogInfo: register('dialog-info', 'info'), dialogClose: register('dialog-close', 'close'), - treeItemExpanded: register('tree-item-expanded', 'chevron-down'), // collapsed is done with rotation - treeFilterOnTypeOn: register('tree-filter-on-type-on', 'list-filter'), treeFilterOnTypeOff: register('tree-filter-on-type-off', 'list-selection'), treeFilterClear: register('tree-filter-clear', 'close'), - treeItemLoading: register('tree-item-loading', 'loading'), - menuSelection: register('menu-selection', 'check'), menuSubmenu: register('menu-submenu', 'chevron-right'), - menuBarMore: register('menubar-more', 'more'), - scrollbarButtonLeft: register('scrollbar-button-left', 'triangle-left'), scrollbarButtonRight: register('scrollbar-button-right', 'triangle-right'), - scrollbarButtonUp: register('scrollbar-button-up', 'triangle-up'), scrollbarButtonDown: register('scrollbar-button-down', 'triangle-down'), - toolBarMore: register('toolbar-more', 'more'), - - quickInputBack: register('quick-input-back', 'arrow-left') + quickInputBack: register('quick-input-back', 'arrow-left'), + dropDownButton: register('drop-down-button', 0xeab4), + symbolCustomColor: register('symbol-customcolor', 0xeb5c), + exportIcon: register('export', 0xebac), + workspaceUnspecified: register('workspace-unspecified', 0xebc3), + newLine: register('newline', 0xebea), + thumbsDownFilled: register('thumbsdown-filled', 0xec13), + thumbsUpFilled: register('thumbsup-filled', 0xec14), + gitFetch: register('git-fetch', 0xec1d), + lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), + debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), } as const; +/** + * The Codicon library is a set of default icons that are built-in in VS Code. + * + * In the product (outside of base) Codicons should only be used as defaults. In order to have all icons in VS Code + * themeable, component should define new, UI component specific icons using `iconRegistry.registerIcon`. + * In that call a Codicon can be named as default. + */ +export const Codicon = { + ...codiconsLibrary, + ...codiconsDerived + +} as const; diff --git a/code/src/vs/base/common/codiconsLibrary.ts b/code/src/vs/base/common/codiconsLibrary.ts new file mode 100644 index 00000000000..a3e87b12ef7 --- /dev/null +++ b/code/src/vs/base/common/codiconsLibrary.ts @@ -0,0 +1,570 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { register } from 'vs/base/common/codiconsUtil'; + + +// This file is automatically generated by (microsoft/vscode-codicons)/scripts/export-to-ts.js +// Please don't edit it, as your changes will be overwritten. +// Instead, add mappings to codiconsDerived in codicons.ts. +export const codiconsLibrary = { + add: register('add', 0xea60), + plus: register('plus', 0xea60), + gistNew: register('gist-new', 0xea60), + repoCreate: register('repo-create', 0xea60), + lightbulb: register('lightbulb', 0xea61), + lightBulb: register('light-bulb', 0xea61), + repo: register('repo', 0xea62), + repoDelete: register('repo-delete', 0xea62), + gistFork: register('gist-fork', 0xea63), + repoForked: register('repo-forked', 0xea63), + gitPullRequest: register('git-pull-request', 0xea64), + gitPullRequestAbandoned: register('git-pull-request-abandoned', 0xea64), + recordKeys: register('record-keys', 0xea65), + keyboard: register('keyboard', 0xea65), + tag: register('tag', 0xea66), + gitPullRequestLabel: register('git-pull-request-label', 0xea66), + tagAdd: register('tag-add', 0xea66), + tagRemove: register('tag-remove', 0xea66), + person: register('person', 0xea67), + personFollow: register('person-follow', 0xea67), + personOutline: register('person-outline', 0xea67), + personFilled: register('person-filled', 0xea67), + gitBranch: register('git-branch', 0xea68), + gitBranchCreate: register('git-branch-create', 0xea68), + gitBranchDelete: register('git-branch-delete', 0xea68), + sourceControl: register('source-control', 0xea68), + mirror: register('mirror', 0xea69), + mirrorPublic: register('mirror-public', 0xea69), + star: register('star', 0xea6a), + starAdd: register('star-add', 0xea6a), + starDelete: register('star-delete', 0xea6a), + starEmpty: register('star-empty', 0xea6a), + comment: register('comment', 0xea6b), + commentAdd: register('comment-add', 0xea6b), + alert: register('alert', 0xea6c), + warning: register('warning', 0xea6c), + search: register('search', 0xea6d), + searchSave: register('search-save', 0xea6d), + logOut: register('log-out', 0xea6e), + signOut: register('sign-out', 0xea6e), + logIn: register('log-in', 0xea6f), + signIn: register('sign-in', 0xea6f), + eye: register('eye', 0xea70), + eyeUnwatch: register('eye-unwatch', 0xea70), + eyeWatch: register('eye-watch', 0xea70), + circleFilled: register('circle-filled', 0xea71), + primitiveDot: register('primitive-dot', 0xea71), + closeDirty: register('close-dirty', 0xea71), + debugBreakpoint: register('debug-breakpoint', 0xea71), + debugBreakpointDisabled: register('debug-breakpoint-disabled', 0xea71), + debugHint: register('debug-hint', 0xea71), + terminalDecorationSuccess: register('terminal-decoration-success', 0xea71), + primitiveSquare: register('primitive-square', 0xea72), + edit: register('edit', 0xea73), + pencil: register('pencil', 0xea73), + info: register('info', 0xea74), + issueOpened: register('issue-opened', 0xea74), + gistPrivate: register('gist-private', 0xea75), + gitForkPrivate: register('git-fork-private', 0xea75), + lock: register('lock', 0xea75), + mirrorPrivate: register('mirror-private', 0xea75), + close: register('close', 0xea76), + removeClose: register('remove-close', 0xea76), + x: register('x', 0xea76), + repoSync: register('repo-sync', 0xea77), + sync: register('sync', 0xea77), + clone: register('clone', 0xea78), + desktopDownload: register('desktop-download', 0xea78), + beaker: register('beaker', 0xea79), + microscope: register('microscope', 0xea79), + vm: register('vm', 0xea7a), + deviceDesktop: register('device-desktop', 0xea7a), + file: register('file', 0xea7b), + fileText: register('file-text', 0xea7b), + more: register('more', 0xea7c), + ellipsis: register('ellipsis', 0xea7c), + kebabHorizontal: register('kebab-horizontal', 0xea7c), + mailReply: register('mail-reply', 0xea7d), + reply: register('reply', 0xea7d), + organization: register('organization', 0xea7e), + organizationFilled: register('organization-filled', 0xea7e), + organizationOutline: register('organization-outline', 0xea7e), + newFile: register('new-file', 0xea7f), + fileAdd: register('file-add', 0xea7f), + newFolder: register('new-folder', 0xea80), + fileDirectoryCreate: register('file-directory-create', 0xea80), + trash: register('trash', 0xea81), + trashcan: register('trashcan', 0xea81), + history: register('history', 0xea82), + clock: register('clock', 0xea82), + folder: register('folder', 0xea83), + fileDirectory: register('file-directory', 0xea83), + symbolFolder: register('symbol-folder', 0xea83), + logoGithub: register('logo-github', 0xea84), + markGithub: register('mark-github', 0xea84), + github: register('github', 0xea84), + terminal: register('terminal', 0xea85), + console: register('console', 0xea85), + repl: register('repl', 0xea85), + zap: register('zap', 0xea86), + symbolEvent: register('symbol-event', 0xea86), + error: register('error', 0xea87), + stop: register('stop', 0xea87), + variable: register('variable', 0xea88), + symbolVariable: register('symbol-variable', 0xea88), + array: register('array', 0xea8a), + symbolArray: register('symbol-array', 0xea8a), + symbolModule: register('symbol-module', 0xea8b), + symbolPackage: register('symbol-package', 0xea8b), + symbolNamespace: register('symbol-namespace', 0xea8b), + symbolObject: register('symbol-object', 0xea8b), + symbolMethod: register('symbol-method', 0xea8c), + symbolFunction: register('symbol-function', 0xea8c), + symbolConstructor: register('symbol-constructor', 0xea8c), + symbolBoolean: register('symbol-boolean', 0xea8f), + symbolNull: register('symbol-null', 0xea8f), + symbolNumeric: register('symbol-numeric', 0xea90), + symbolNumber: register('symbol-number', 0xea90), + symbolStructure: register('symbol-structure', 0xea91), + symbolStruct: register('symbol-struct', 0xea91), + symbolParameter: register('symbol-parameter', 0xea92), + symbolTypeParameter: register('symbol-type-parameter', 0xea92), + symbolKey: register('symbol-key', 0xea93), + symbolText: register('symbol-text', 0xea93), + symbolReference: register('symbol-reference', 0xea94), + goToFile: register('go-to-file', 0xea94), + symbolEnum: register('symbol-enum', 0xea95), + symbolValue: register('symbol-value', 0xea95), + symbolRuler: register('symbol-ruler', 0xea96), + symbolUnit: register('symbol-unit', 0xea96), + activateBreakpoints: register('activate-breakpoints', 0xea97), + archive: register('archive', 0xea98), + arrowBoth: register('arrow-both', 0xea99), + arrowDown: register('arrow-down', 0xea9a), + arrowLeft: register('arrow-left', 0xea9b), + arrowRight: register('arrow-right', 0xea9c), + arrowSmallDown: register('arrow-small-down', 0xea9d), + arrowSmallLeft: register('arrow-small-left', 0xea9e), + arrowSmallRight: register('arrow-small-right', 0xea9f), + arrowSmallUp: register('arrow-small-up', 0xeaa0), + arrowUp: register('arrow-up', 0xeaa1), + bell: register('bell', 0xeaa2), + bold: register('bold', 0xeaa3), + book: register('book', 0xeaa4), + bookmark: register('bookmark', 0xeaa5), + debugBreakpointConditionalUnverified: register('debug-breakpoint-conditional-unverified', 0xeaa6), + debugBreakpointConditional: register('debug-breakpoint-conditional', 0xeaa7), + debugBreakpointConditionalDisabled: register('debug-breakpoint-conditional-disabled', 0xeaa7), + debugBreakpointDataUnverified: register('debug-breakpoint-data-unverified', 0xeaa8), + debugBreakpointData: register('debug-breakpoint-data', 0xeaa9), + debugBreakpointDataDisabled: register('debug-breakpoint-data-disabled', 0xeaa9), + debugBreakpointLogUnverified: register('debug-breakpoint-log-unverified', 0xeaaa), + debugBreakpointLog: register('debug-breakpoint-log', 0xeaab), + debugBreakpointLogDisabled: register('debug-breakpoint-log-disabled', 0xeaab), + briefcase: register('briefcase', 0xeaac), + broadcast: register('broadcast', 0xeaad), + browser: register('browser', 0xeaae), + bug: register('bug', 0xeaaf), + calendar: register('calendar', 0xeab0), + caseSensitive: register('case-sensitive', 0xeab1), + check: register('check', 0xeab2), + checklist: register('checklist', 0xeab3), + chevronDown: register('chevron-down', 0xeab4), + chevronLeft: register('chevron-left', 0xeab5), + chevronRight: register('chevron-right', 0xeab6), + chevronUp: register('chevron-up', 0xeab7), + chromeClose: register('chrome-close', 0xeab8), + chromeMaximize: register('chrome-maximize', 0xeab9), + chromeMinimize: register('chrome-minimize', 0xeaba), + chromeRestore: register('chrome-restore', 0xeabb), + circleOutline: register('circle-outline', 0xeabc), + circle: register('circle', 0xeabc), + debugBreakpointUnverified: register('debug-breakpoint-unverified', 0xeabc), + terminalDecorationIncomplete: register('terminal-decoration-incomplete', 0xeabc), + circleSlash: register('circle-slash', 0xeabd), + circuitBoard: register('circuit-board', 0xeabe), + clearAll: register('clear-all', 0xeabf), + clippy: register('clippy', 0xeac0), + closeAll: register('close-all', 0xeac1), + cloudDownload: register('cloud-download', 0xeac2), + cloudUpload: register('cloud-upload', 0xeac3), + code: register('code', 0xeac4), + collapseAll: register('collapse-all', 0xeac5), + colorMode: register('color-mode', 0xeac6), + commentDiscussion: register('comment-discussion', 0xeac7), + creditCard: register('credit-card', 0xeac9), + dash: register('dash', 0xeacc), + dashboard: register('dashboard', 0xeacd), + database: register('database', 0xeace), + debugContinue: register('debug-continue', 0xeacf), + debugDisconnect: register('debug-disconnect', 0xead0), + debugPause: register('debug-pause', 0xead1), + debugRestart: register('debug-restart', 0xead2), + debugStart: register('debug-start', 0xead3), + debugStepInto: register('debug-step-into', 0xead4), + debugStepOut: register('debug-step-out', 0xead5), + debugStepOver: register('debug-step-over', 0xead6), + debugStop: register('debug-stop', 0xead7), + debug: register('debug', 0xead8), + deviceCameraVideo: register('device-camera-video', 0xead9), + deviceCamera: register('device-camera', 0xeada), + deviceMobile: register('device-mobile', 0xeadb), + diffAdded: register('diff-added', 0xeadc), + diffIgnored: register('diff-ignored', 0xeadd), + diffModified: register('diff-modified', 0xeade), + diffRemoved: register('diff-removed', 0xeadf), + diffRenamed: register('diff-renamed', 0xeae0), + diff: register('diff', 0xeae1), + diffSidebyside: register('diff-sidebyside', 0xeae1), + discard: register('discard', 0xeae2), + editorLayout: register('editor-layout', 0xeae3), + emptyWindow: register('empty-window', 0xeae4), + exclude: register('exclude', 0xeae5), + extensions: register('extensions', 0xeae6), + eyeClosed: register('eye-closed', 0xeae7), + fileBinary: register('file-binary', 0xeae8), + fileCode: register('file-code', 0xeae9), + fileMedia: register('file-media', 0xeaea), + filePdf: register('file-pdf', 0xeaeb), + fileSubmodule: register('file-submodule', 0xeaec), + fileSymlinkDirectory: register('file-symlink-directory', 0xeaed), + fileSymlinkFile: register('file-symlink-file', 0xeaee), + fileZip: register('file-zip', 0xeaef), + files: register('files', 0xeaf0), + filter: register('filter', 0xeaf1), + flame: register('flame', 0xeaf2), + foldDown: register('fold-down', 0xeaf3), + foldUp: register('fold-up', 0xeaf4), + fold: register('fold', 0xeaf5), + folderActive: register('folder-active', 0xeaf6), + folderOpened: register('folder-opened', 0xeaf7), + gear: register('gear', 0xeaf8), + gift: register('gift', 0xeaf9), + gistSecret: register('gist-secret', 0xeafa), + gist: register('gist', 0xeafb), + gitCommit: register('git-commit', 0xeafc), + gitCompare: register('git-compare', 0xeafd), + compareChanges: register('compare-changes', 0xeafd), + gitMerge: register('git-merge', 0xeafe), + githubAction: register('github-action', 0xeaff), + githubAlt: register('github-alt', 0xeb00), + globe: register('globe', 0xeb01), + grabber: register('grabber', 0xeb02), + graph: register('graph', 0xeb03), + gripper: register('gripper', 0xeb04), + heart: register('heart', 0xeb05), + home: register('home', 0xeb06), + horizontalRule: register('horizontal-rule', 0xeb07), + hubot: register('hubot', 0xeb08), + inbox: register('inbox', 0xeb09), + issueReopened: register('issue-reopened', 0xeb0b), + issues: register('issues', 0xeb0c), + italic: register('italic', 0xeb0d), + jersey: register('jersey', 0xeb0e), + json: register('json', 0xeb0f), + kebabVertical: register('kebab-vertical', 0xeb10), + key: register('key', 0xeb11), + law: register('law', 0xeb12), + lightbulbAutofix: register('lightbulb-autofix', 0xeb13), + linkExternal: register('link-external', 0xeb14), + link: register('link', 0xeb15), + listOrdered: register('list-ordered', 0xeb16), + listUnordered: register('list-unordered', 0xeb17), + liveShare: register('live-share', 0xeb18), + loading: register('loading', 0xeb19), + location: register('location', 0xeb1a), + mailRead: register('mail-read', 0xeb1b), + mail: register('mail', 0xeb1c), + markdown: register('markdown', 0xeb1d), + megaphone: register('megaphone', 0xeb1e), + mention: register('mention', 0xeb1f), + milestone: register('milestone', 0xeb20), + gitPullRequestMilestone: register('git-pull-request-milestone', 0xeb20), + mortarBoard: register('mortar-board', 0xeb21), + move: register('move', 0xeb22), + multipleWindows: register('multiple-windows', 0xeb23), + mute: register('mute', 0xeb24), + noNewline: register('no-newline', 0xeb25), + note: register('note', 0xeb26), + octoface: register('octoface', 0xeb27), + openPreview: register('open-preview', 0xeb28), + package: register('package', 0xeb29), + paintcan: register('paintcan', 0xeb2a), + pin: register('pin', 0xeb2b), + play: register('play', 0xeb2c), + run: register('run', 0xeb2c), + plug: register('plug', 0xeb2d), + preserveCase: register('preserve-case', 0xeb2e), + preview: register('preview', 0xeb2f), + project: register('project', 0xeb30), + pulse: register('pulse', 0xeb31), + question: register('question', 0xeb32), + quote: register('quote', 0xeb33), + radioTower: register('radio-tower', 0xeb34), + reactions: register('reactions', 0xeb35), + references: register('references', 0xeb36), + refresh: register('refresh', 0xeb37), + regex: register('regex', 0xeb38), + remoteExplorer: register('remote-explorer', 0xeb39), + remote: register('remote', 0xeb3a), + remove: register('remove', 0xeb3b), + replaceAll: register('replace-all', 0xeb3c), + replace: register('replace', 0xeb3d), + repoClone: register('repo-clone', 0xeb3e), + repoForcePush: register('repo-force-push', 0xeb3f), + repoPull: register('repo-pull', 0xeb40), + repoPush: register('repo-push', 0xeb41), + report: register('report', 0xeb42), + requestChanges: register('request-changes', 0xeb43), + rocket: register('rocket', 0xeb44), + rootFolderOpened: register('root-folder-opened', 0xeb45), + rootFolder: register('root-folder', 0xeb46), + rss: register('rss', 0xeb47), + ruby: register('ruby', 0xeb48), + saveAll: register('save-all', 0xeb49), + saveAs: register('save-as', 0xeb4a), + save: register('save', 0xeb4b), + screenFull: register('screen-full', 0xeb4c), + screenNormal: register('screen-normal', 0xeb4d), + searchStop: register('search-stop', 0xeb4e), + server: register('server', 0xeb50), + settingsGear: register('settings-gear', 0xeb51), + settings: register('settings', 0xeb52), + shield: register('shield', 0xeb53), + smiley: register('smiley', 0xeb54), + sortPrecedence: register('sort-precedence', 0xeb55), + splitHorizontal: register('split-horizontal', 0xeb56), + splitVertical: register('split-vertical', 0xeb57), + squirrel: register('squirrel', 0xeb58), + starFull: register('star-full', 0xeb59), + starHalf: register('star-half', 0xeb5a), + symbolClass: register('symbol-class', 0xeb5b), + symbolColor: register('symbol-color', 0xeb5c), + symbolConstant: register('symbol-constant', 0xeb5d), + symbolEnumMember: register('symbol-enum-member', 0xeb5e), + symbolField: register('symbol-field', 0xeb5f), + symbolFile: register('symbol-file', 0xeb60), + symbolInterface: register('symbol-interface', 0xeb61), + symbolKeyword: register('symbol-keyword', 0xeb62), + symbolMisc: register('symbol-misc', 0xeb63), + symbolOperator: register('symbol-operator', 0xeb64), + symbolProperty: register('symbol-property', 0xeb65), + wrench: register('wrench', 0xeb65), + wrenchSubaction: register('wrench-subaction', 0xeb65), + symbolSnippet: register('symbol-snippet', 0xeb66), + tasklist: register('tasklist', 0xeb67), + telescope: register('telescope', 0xeb68), + textSize: register('text-size', 0xeb69), + threeBars: register('three-bars', 0xeb6a), + thumbsdown: register('thumbsdown', 0xeb6b), + thumbsup: register('thumbsup', 0xeb6c), + tools: register('tools', 0xeb6d), + triangleDown: register('triangle-down', 0xeb6e), + triangleLeft: register('triangle-left', 0xeb6f), + triangleRight: register('triangle-right', 0xeb70), + triangleUp: register('triangle-up', 0xeb71), + twitter: register('twitter', 0xeb72), + unfold: register('unfold', 0xeb73), + unlock: register('unlock', 0xeb74), + unmute: register('unmute', 0xeb75), + unverified: register('unverified', 0xeb76), + verified: register('verified', 0xeb77), + versions: register('versions', 0xeb78), + vmActive: register('vm-active', 0xeb79), + vmOutline: register('vm-outline', 0xeb7a), + vmRunning: register('vm-running', 0xeb7b), + watch: register('watch', 0xeb7c), + whitespace: register('whitespace', 0xeb7d), + wholeWord: register('whole-word', 0xeb7e), + window: register('window', 0xeb7f), + wordWrap: register('word-wrap', 0xeb80), + zoomIn: register('zoom-in', 0xeb81), + zoomOut: register('zoom-out', 0xeb82), + listFilter: register('list-filter', 0xeb83), + listFlat: register('list-flat', 0xeb84), + listSelection: register('list-selection', 0xeb85), + selection: register('selection', 0xeb85), + listTree: register('list-tree', 0xeb86), + debugBreakpointFunctionUnverified: register('debug-breakpoint-function-unverified', 0xeb87), + debugBreakpointFunction: register('debug-breakpoint-function', 0xeb88), + debugBreakpointFunctionDisabled: register('debug-breakpoint-function-disabled', 0xeb88), + debugStackframeActive: register('debug-stackframe-active', 0xeb89), + circleSmallFilled: register('circle-small-filled', 0xeb8a), + debugStackframeDot: register('debug-stackframe-dot', 0xeb8a), + terminalDecorationMark: register('terminal-decoration-mark', 0xeb8a), + debugStackframe: register('debug-stackframe', 0xeb8b), + debugStackframeFocused: register('debug-stackframe-focused', 0xeb8b), + debugBreakpointUnsupported: register('debug-breakpoint-unsupported', 0xeb8c), + symbolString: register('symbol-string', 0xeb8d), + debugReverseContinue: register('debug-reverse-continue', 0xeb8e), + debugStepBack: register('debug-step-back', 0xeb8f), + debugRestartFrame: register('debug-restart-frame', 0xeb90), + debugAlt: register('debug-alt', 0xeb91), + callIncoming: register('call-incoming', 0xeb92), + callOutgoing: register('call-outgoing', 0xeb93), + menu: register('menu', 0xeb94), + expandAll: register('expand-all', 0xeb95), + feedback: register('feedback', 0xeb96), + gitPullRequestReviewer: register('git-pull-request-reviewer', 0xeb96), + groupByRefType: register('group-by-ref-type', 0xeb97), + ungroupByRefType: register('ungroup-by-ref-type', 0xeb98), + account: register('account', 0xeb99), + gitPullRequestAssignee: register('git-pull-request-assignee', 0xeb99), + bellDot: register('bell-dot', 0xeb9a), + debugConsole: register('debug-console', 0xeb9b), + library: register('library', 0xeb9c), + output: register('output', 0xeb9d), + runAll: register('run-all', 0xeb9e), + syncIgnored: register('sync-ignored', 0xeb9f), + pinned: register('pinned', 0xeba0), + githubInverted: register('github-inverted', 0xeba1), + serverProcess: register('server-process', 0xeba2), + serverEnvironment: register('server-environment', 0xeba3), + pass: register('pass', 0xeba4), + issueClosed: register('issue-closed', 0xeba4), + stopCircle: register('stop-circle', 0xeba5), + playCircle: register('play-circle', 0xeba6), + record: register('record', 0xeba7), + debugAltSmall: register('debug-alt-small', 0xeba8), + vmConnect: register('vm-connect', 0xeba9), + cloud: register('cloud', 0xebaa), + merge: register('merge', 0xebab), + export: register('export', 0xebac), + graphLeft: register('graph-left', 0xebad), + magnet: register('magnet', 0xebae), + notebook: register('notebook', 0xebaf), + redo: register('redo', 0xebb0), + checkAll: register('check-all', 0xebb1), + pinnedDirty: register('pinned-dirty', 0xebb2), + passFilled: register('pass-filled', 0xebb3), + circleLargeFilled: register('circle-large-filled', 0xebb4), + circleLarge: register('circle-large', 0xebb5), + circleLargeOutline: register('circle-large-outline', 0xebb5), + combine: register('combine', 0xebb6), + gather: register('gather', 0xebb6), + table: register('table', 0xebb7), + variableGroup: register('variable-group', 0xebb8), + typeHierarchy: register('type-hierarchy', 0xebb9), + typeHierarchySub: register('type-hierarchy-sub', 0xebba), + typeHierarchySuper: register('type-hierarchy-super', 0xebbb), + gitPullRequestCreate: register('git-pull-request-create', 0xebbc), + runAbove: register('run-above', 0xebbd), + runBelow: register('run-below', 0xebbe), + notebookTemplate: register('notebook-template', 0xebbf), + debugRerun: register('debug-rerun', 0xebc0), + workspaceTrusted: register('workspace-trusted', 0xebc1), + workspaceUntrusted: register('workspace-untrusted', 0xebc2), + workspaceUnknown: register('workspace-unknown', 0xebc3), + terminalCmd: register('terminal-cmd', 0xebc4), + terminalDebian: register('terminal-debian', 0xebc5), + terminalLinux: register('terminal-linux', 0xebc6), + terminalPowershell: register('terminal-powershell', 0xebc7), + terminalTmux: register('terminal-tmux', 0xebc8), + terminalUbuntu: register('terminal-ubuntu', 0xebc9), + terminalBash: register('terminal-bash', 0xebca), + arrowSwap: register('arrow-swap', 0xebcb), + copy: register('copy', 0xebcc), + personAdd: register('person-add', 0xebcd), + filterFilled: register('filter-filled', 0xebce), + wand: register('wand', 0xebcf), + debugLineByLine: register('debug-line-by-line', 0xebd0), + inspect: register('inspect', 0xebd1), + layers: register('layers', 0xebd2), + layersDot: register('layers-dot', 0xebd3), + layersActive: register('layers-active', 0xebd4), + compass: register('compass', 0xebd5), + compassDot: register('compass-dot', 0xebd6), + compassActive: register('compass-active', 0xebd7), + azure: register('azure', 0xebd8), + issueDraft: register('issue-draft', 0xebd9), + gitPullRequestClosed: register('git-pull-request-closed', 0xebda), + gitPullRequestDraft: register('git-pull-request-draft', 0xebdb), + debugAll: register('debug-all', 0xebdc), + debugCoverage: register('debug-coverage', 0xebdd), + runErrors: register('run-errors', 0xebde), + folderLibrary: register('folder-library', 0xebdf), + debugContinueSmall: register('debug-continue-small', 0xebe0), + beakerStop: register('beaker-stop', 0xebe1), + graphLine: register('graph-line', 0xebe2), + graphScatter: register('graph-scatter', 0xebe3), + pieChart: register('pie-chart', 0xebe4), + bracket: register('bracket', 0xeb0f), + bracketDot: register('bracket-dot', 0xebe5), + bracketError: register('bracket-error', 0xebe6), + lockSmall: register('lock-small', 0xebe7), + azureDevops: register('azure-devops', 0xebe8), + verifiedFilled: register('verified-filled', 0xebe9), + newline: register('newline', 0xebea), + layout: register('layout', 0xebeb), + layoutActivitybarLeft: register('layout-activitybar-left', 0xebec), + layoutActivitybarRight: register('layout-activitybar-right', 0xebed), + layoutPanelLeft: register('layout-panel-left', 0xebee), + layoutPanelCenter: register('layout-panel-center', 0xebef), + layoutPanelJustify: register('layout-panel-justify', 0xebf0), + layoutPanelRight: register('layout-panel-right', 0xebf1), + layoutPanel: register('layout-panel', 0xebf2), + layoutSidebarLeft: register('layout-sidebar-left', 0xebf3), + layoutSidebarRight: register('layout-sidebar-right', 0xebf4), + layoutStatusbar: register('layout-statusbar', 0xebf5), + layoutMenubar: register('layout-menubar', 0xebf6), + layoutCentered: register('layout-centered', 0xebf7), + target: register('target', 0xebf8), + indent: register('indent', 0xebf9), + recordSmall: register('record-small', 0xebfa), + errorSmall: register('error-small', 0xebfb), + terminalDecorationError: register('terminal-decoration-error', 0xebfb), + arrowCircleDown: register('arrow-circle-down', 0xebfc), + arrowCircleLeft: register('arrow-circle-left', 0xebfd), + arrowCircleRight: register('arrow-circle-right', 0xebfe), + arrowCircleUp: register('arrow-circle-up', 0xebff), + layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), + layoutPanelOff: register('layout-panel-off', 0xec01), + layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), + blank: register('blank', 0xec03), + heartFilled: register('heart-filled', 0xec04), + map: register('map', 0xec05), + mapFilled: register('map-filled', 0xec06), + circleSmall: register('circle-small', 0xec07), + bellSlash: register('bell-slash', 0xec08), + bellSlashDot: register('bell-slash-dot', 0xec09), + commentUnresolved: register('comment-unresolved', 0xec0a), + gitPullRequestGoToChanges: register('git-pull-request-go-to-changes', 0xec0b), + gitPullRequestNewChanges: register('git-pull-request-new-changes', 0xec0c), + searchFuzzy: register('search-fuzzy', 0xec0d), + commentDraft: register('comment-draft', 0xec0e), + send: register('send', 0xec0f), + sparkle: register('sparkle', 0xec10), + insert: register('insert', 0xec11), + mic: register('mic', 0xec12), + thumbsdownFilled: register('thumbsdown-filled', 0xec13), + thumbsupFilled: register('thumbsup-filled', 0xec14), + coffee: register('coffee', 0xec15), + snake: register('snake', 0xec16), + game: register('game', 0xec17), + vr: register('vr', 0xec18), + chip: register('chip', 0xec19), + piano: register('piano', 0xec1a), + music: register('music', 0xec1b), + micFilled: register('mic-filled', 0xec1c), + repoFetch: register('repo-fetch', 0xec1d), + copilot: register('copilot', 0xec1e), + lightbulbSparkle: register('lightbulb-sparkle', 0xec1f), + robot: register('robot', 0xec20), + sparkleFilled: register('sparkle-filled', 0xec21), + diffSingle: register('diff-single', 0xec22), + diffMultiple: register('diff-multiple', 0xec23), + surroundWith: register('surround-with', 0xec24), + share: register('share', 0xec25), + gitStash: register('git-stash', 0xec26), + gitStashApply: register('git-stash-apply', 0xec27), + gitStashPop: register('git-stash-pop', 0xec28), + vscode: register('vscode', 0xec29), + vscodeInsiders: register('vscode-insiders', 0xec2a), + codeOss: register('code-oss', 0xec2b), + runCoverage: register('run-coverage', 0xec2c), + runAllCoverage: register('run-all-coverage', 0xec2d), + coverage: register('coverage', 0xec2e), + githubProject: register('github-project', 0xec2f), +} as const; diff --git a/code/src/vs/base/common/codiconsUtil.ts b/code/src/vs/base/common/codiconsUtil.ts new file mode 100644 index 00000000000..ce7f9b2dafb --- /dev/null +++ b/code/src/vs/base/common/codiconsUtil.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ThemeIcon } from 'vs/base/common/themables'; +import { isString } from 'vs/base/common/types'; + + +const _codiconFontCharacters: { [id: string]: number } = Object.create(null); + +export function register(id: string, fontCharacter: number | string): ThemeIcon { + if (isString(fontCharacter)) { + const val = _codiconFontCharacters[fontCharacter]; + if (val === undefined) { + throw new Error(`${id} references an unknown codicon: ${fontCharacter}`); + } + fontCharacter = val; + } + _codiconFontCharacters[id] = fontCharacter; + return { id }; +} + +/** + * Only to be used by the iconRegistry. + */ +export function getCodiconFontCharacters(): { [id: string]: number } { + return _codiconFontCharacters; +} diff --git a/code/src/vs/base/common/dataTransfer.ts b/code/src/vs/base/common/dataTransfer.ts index bed42389897..9c9ac45640b 100644 --- a/code/src/vs/base/common/dataTransfer.ts +++ b/code/src/vs/base/common/dataTransfer.ts @@ -50,6 +50,7 @@ export interface IReadonlyVSDataTransfer extends Iterable(listeners: ListenerOrListeners, fn: (c: ListenerC } }; + +const _listenerFinalizers = _enableListenerGCedWarning + ? new FinalizationRegistry(heldValue => { + if (typeof heldValue === 'string') { + console.warn('[LEAKING LISTENER] GC\'ed a listener that was NOT yet disposed. This is where is was created:'); + console.warn(heldValue); + } + }) + : undefined; + /** * The Emitter can be used to expose an Event to the public * to fire it from the insides. @@ -1054,13 +1073,23 @@ export class Emitter { this._size++; - const result = toDisposable(() => { removeMonitor?.(); this._removeListener(contained); }); + + const result = toDisposable(() => { + _listenerFinalizers?.unregister(result); + removeMonitor?.(); + this._removeListener(contained); + }); if (disposables instanceof DisposableStore) { disposables.add(result); } else if (Array.isArray(disposables)) { disposables.push(result); } + if (_listenerFinalizers) { + const stack = new Error().stack!.split('\n').slice(2).join('\n').trim(); + _listenerFinalizers.register(result, stack, result); + } + return result; }; diff --git a/code/src/vs/base/common/hierarchicalKind.ts b/code/src/vs/base/common/hierarchicalKind.ts new file mode 100644 index 00000000000..a2edd614375 --- /dev/null +++ b/code/src/vs/base/common/hierarchicalKind.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class HierarchicalKind { + public static readonly sep = '.'; + + public static readonly None = new HierarchicalKind('@@none@@'); // Special kind that matches nothing + public static readonly Empty = new HierarchicalKind(''); + + constructor( + public readonly value: string + ) { } + + public equals(other: HierarchicalKind): boolean { + return this.value === other.value; + } + + public contains(other: HierarchicalKind): boolean { + return this.equals(other) || this.value === '' || other.value.startsWith(this.value + HierarchicalKind.sep); + } + + public intersects(other: HierarchicalKind): boolean { + return this.contains(other) || other.contains(this); + } + + public append(...parts: string[]): HierarchicalKind { + return new HierarchicalKind((this.value ? [this.value, ...parts] : parts).join(HierarchicalKind.sep)); + } +} diff --git a/code/src/vs/base/common/jsonSchema.ts b/code/src/vs/base/common/jsonSchema.ts index 81262c2f46a..4216b0e5c0d 100644 --- a/code/src/vs/base/common/jsonSchema.ts +++ b/code/src/vs/base/common/jsonSchema.ts @@ -99,3 +99,22 @@ export interface IJSONSchemaSnippet { body?: any; // a object that will be JSON stringified bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t) } + +/** + * Converts a basic JSON schema to a TypeScript type. + * + * TODO: only supports basic schemas. Doesn't support all JSON schema features. + */ +export type SchemaToType = T extends { type: 'string' } + ? string + : T extends { type: 'number' } + ? number + : T extends { type: 'boolean' } + ? boolean + : T extends { type: 'null' } + ? null + : T extends { type: 'object'; properties: infer P } + ? { [K in keyof P]: SchemaToType } + : T extends { type: 'array'; items: infer I } + ? Array> + : never; diff --git a/code/src/vs/base/common/network.ts b/code/src/vs/base/common/network.ts index 1303b2a13c7..e39cb9fa11e 100644 --- a/code/src/vs/base/common/network.ts +++ b/code/src/vs/base/common/network.ts @@ -117,11 +117,6 @@ export namespace Schemas { * Scheme used for special rendering of settings in the release notes */ export const codeSetting = 'code-setting'; - - /** - * Scheme used for special rendering of features in the release notes - */ - export const codeFeature = 'code-feature'; } export function matchesScheme(target: URI | string, scheme: string): boolean { diff --git a/code/src/vs/base/common/observableInternal/base.ts b/code/src/vs/base/common/observableInternal/base.ts index 74b03df1438..4e738e63344 100644 --- a/code/src/vs/base/common/observableInternal/base.ts +++ b/code/src/vs/base/common/observableInternal/base.ts @@ -389,12 +389,12 @@ export class ObservableValue constructor( private readonly _owner: Owner, private readonly _debugName: string | undefined, - initialValue: T + initialValue: T, ) { super(); this._value = initialValue; } - public get(): T { + public override get(): T { return this._value; } diff --git a/code/src/vs/base/common/observableInternal/utils.ts b/code/src/vs/base/common/observableInternal/utils.ts index 5831de89add..0ac31f7f881 100644 --- a/code/src/vs/base/common/observableInternal/utils.ts +++ b/code/src/vs/base/common/observableInternal/utils.ts @@ -253,6 +253,9 @@ class ObservableSignal extends BaseObservable implements } } +/** + * @deprecated Use `debouncedObservable2` instead. + */ export function debouncedObservable(observable: IObservable, debounceMs: number, disposableStore: DisposableStore): IObservable { const debouncedObservable = observableValue('debounced', undefined); @@ -276,6 +279,48 @@ export function debouncedObservable(observable: IObservable, debounceMs: n return debouncedObservable; } +/** + * Creates an observable that debounces the input observable. + */ +export function debouncedObservable2(observable: IObservable, debounceMs: number): IObservable { + let hasValue = false; + let lastValue: T | undefined; + + let timeout: any = undefined; + + return observableFromEvent(cb => { + const d = autorun(reader => { + const value = observable.read(reader); + + if (!hasValue) { + hasValue = true; + lastValue = value; + } else { + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(() => { + lastValue = value; + cb(); + }, debounceMs); + } + }); + return { + dispose() { + d.dispose(); + hasValue = false; + lastValue = undefined; + }, + }; + }, () => { + if (hasValue) { + return lastValue!; + } else { + return observable.get(); + } + }); +} + export function wasEventTriggeredRecently(event: Event, timeoutMs: number, disposableStore: DisposableStore): IObservable { const observable = observableValue('triggeredRecently', false); diff --git a/code/src/vs/base/common/platform.ts b/code/src/vs/base/common/platform.ts index 3893fbc6fcd..2251c7db5ba 100644 --- a/code/src/vs/base/common/platform.ts +++ b/code/src/vs/base/common/platform.ts @@ -45,6 +45,7 @@ export interface INodeProcess { arch: string; env: IProcessEnvironment; versions?: { + node?: string; electron?: string; chrome?: string; }; @@ -60,7 +61,7 @@ let nodeProcess: INodeProcess | undefined = undefined; if (typeof $globalThis.vscode !== 'undefined' && typeof $globalThis.vscode.process !== 'undefined') { // Native environment (sandboxed) nodeProcess = $globalThis.vscode.process; -} else if (typeof process !== 'undefined') { +} else if (typeof process !== 'undefined' && typeof process?.versions?.node === 'string') { // Native environment (non-sandboxed) nodeProcess = process; } diff --git a/code/src/vs/base/common/product.ts b/code/src/vs/base/common/product.ts index d5617080a70..fa734fea112 100644 --- a/code/src/vs/base/common/product.ts +++ b/code/src/vs/base/common/product.ts @@ -191,6 +191,7 @@ export interface IProductConfiguration { readonly commonlyUsedSettings?: string[]; readonly aiGeneratedWorkspaceTrust?: IAiGeneratedWorkspaceTrust; readonly gitHubEntitlement?: IGitHubEntitlement; + readonly chatWelcomeView?: IChatWelcomeView; } export interface ITunnelApplicationConfig { @@ -304,3 +305,9 @@ export interface IGitHubEntitlement { confirmationMessage: string; confirmationAction: string; } + +export interface IChatWelcomeView { + welcomeViewId: string; + welcomeViewTitle: string; + welcomeViewContent: string; +} diff --git a/code/src/vs/base/common/strings.ts b/code/src/vs/base/common/strings.ts index 6ec11f03919..050de0ca181 100644 --- a/code/src/vs/base/common/strings.ts +++ b/code/src/vs/base/common/strings.ts @@ -766,14 +766,29 @@ export function lcut(text: string, n: number, prefix = '') { } // Escape codes, compiled from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ -const CSI_SEQUENCE = /(:?\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/g; - // Plus additional markers for custom `\x1b]...\x07` instructions. -const CSI_CUSTOM_SEQUENCE = /\x1b\].*?\x07/g; +const CSI_SEQUENCE = /(:?(:?\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; + +/** Iterates over parts of a string with CSI sequences */ +export function* forAnsiStringParts(str: string) { + let last = 0; + for (const match of str.matchAll(CSI_SEQUENCE)) { + if (last !== match.index) { + yield { isCode: false, str: str.substring(last, match.index) }; + } + + yield { isCode: true, str: match[0] }; + last = match.index + match[0].length; + } + + if (last !== str.length) { + yield { isCode: false, str: str.substring(last) }; + } +} export function removeAnsiEscapeCodes(str: string): string { if (str) { - str = str.replace(CSI_SEQUENCE, '').replace(CSI_CUSTOM_SEQUENCE, ''); + str = str.replace(CSI_SEQUENCE, ''); } return str; diff --git a/code/src/vs/base/node/extpath.ts b/code/src/vs/base/node/extpath.ts index ee8f3f4eb31..a7ec9cf6d36 100644 --- a/code/src/vs/base/node/extpath.ts +++ b/code/src/vs/base/node/extpath.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { basename, dirname, join, normalize, sep } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; import { rtrim } from 'vs/base/common/strings'; @@ -58,7 +59,7 @@ export function realcaseSync(path: string): string | null { return null; } -export async function realcase(path: string): Promise { +export async function realcase(path: string, token?: CancellationToken): Promise { if (isLinux) { // This method is unsupported on OS that have case sensitive // file system where the same path can exist in different forms @@ -73,11 +74,15 @@ export async function realcase(path: string): Promise { const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase(); try { + if (token?.isCancellationRequested) { + return null; + } + const entries = await Promises.readdir(dir); const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search if (found.length === 1) { // on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition - const prefix = await realcase(dir); // recurse + const prefix = await realcase(dir, token); // recurse if (prefix) { return join(prefix, found[0]); } @@ -85,7 +90,7 @@ export async function realcase(path: string): Promise { // must be a case sensitive $filesystem const ix = found.indexOf(name); if (ix >= 0) { // case sensitive - const prefix = await realcase(dir); // recurse + const prefix = await realcase(dir, token); // recurse if (prefix) { return join(prefix, found[ix]); } diff --git a/code/src/vs/base/node/zip.ts b/code/src/vs/base/node/zip.ts index 3cb7bc9795b..c0d9b4b8ecb 100644 --- a/code/src/vs/base/node/zip.ts +++ b/code/src/vs/base/node/zip.ts @@ -164,7 +164,7 @@ async function openZip(zipFile: string, lazy: boolean = false): Promise const { open } = await import('yauzl'); return new Promise((resolve, reject) => { - open(zipFile, lazy ? { lazyEntries: true } : undefined!, (error?: Error, zipfile?: ZipFile) => { + open(zipFile, lazy ? { lazyEntries: true } : undefined!, (error: Error | null, zipfile?: ZipFile) => { if (error) { reject(toExtractError(error)); } else { @@ -176,7 +176,7 @@ async function openZip(zipFile: string, lazy: boolean = false): Promise function openZipStream(zipFile: ZipFile, entry: Entry): Promise { return new Promise((resolve, reject) => { - zipFile.openReadStream(entry, (error?: Error, stream?: Readable) => { + zipFile.openReadStream(entry, (error: Error | null, stream?: Readable) => { if (error) { reject(toExtractError(error)); } else { diff --git a/code/src/vs/base/parts/ipc/common/ipc.ts b/code/src/vs/base/parts/ipc/common/ipc.ts index f943347519e..6530fac0d7b 100644 --- a/code/src/vs/base/parts/ipc/common/ipc.ts +++ b/code/src/vs/base/parts/ipc/common/ipc.ts @@ -806,7 +806,7 @@ export class IPCServer implements IChannelServer, I return result; } - constructor(onDidClientConnect: Event) { + constructor(onDidClientConnect: Event, ipcLogger?: IIPCLogger | null, timeoutDelay?: number) { this.disposables.add(onDidClientConnect(({ protocol, onDidClientDisconnect }) => { const onFirstMessage = Event.once(protocol.onMessage); @@ -814,8 +814,8 @@ export class IPCServer implements IChannelServer, I const reader = new BufferReader(msg); const ctx = deserialize(reader) as TContext; - const channelServer = new ChannelServer(protocol, ctx); - const channelClient = new ChannelClient(protocol); + const channelServer = new ChannelServer(protocol, ctx, ipcLogger, timeoutDelay); + const channelClient = new ChannelClient(protocol, ipcLogger); this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel)); @@ -1093,6 +1093,9 @@ export namespace ProxyChannel { // Buffer any event that should be supported by // iterating over all property keys and finding them + // However, this will not work for services that + // are lazy and use a Proxy within. For that we + // still need to check later (see below). const mapEventNameToEvent = new Map>(); for (const key in handler) { if (propertyIsEvent(key)) { @@ -1108,11 +1111,17 @@ export namespace ProxyChannel { return eventImpl as Event; } - if (propertyIsDynamicEvent(event)) { - const target = handler[event]; - if (typeof target === 'function') { + const target = handler[event]; + if (typeof target === 'function') { + if (propertyIsDynamicEvent(event)) { return target.call(handler, arg); } + + if (propertyIsEvent(event)) { + mapEventNameToEvent.set(event, Event.buffer(handler[event] as Event, true, undefined, disposables)); + + return mapEventNameToEvent.get(event) as Event; + } } throw new ErrorNoTelemetry(`Event not found: ${event}`); diff --git a/code/src/vs/base/parts/ipc/node/ipc.cp.ts b/code/src/vs/base/parts/ipc/node/ipc.cp.ts index 4fcad2758da..d51d77e3c81 100644 --- a/code/src/vs/base/parts/ipc/node/ipc.cp.ts +++ b/code/src/vs/base/parts/ipc/node/ipc.cp.ts @@ -207,7 +207,7 @@ export class Client implements IChannelClient, IDisposable { const onMessageEmitter = new Emitter(); const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', msg => msg); - onRawMessage(msg => { + const rawMessageDisposable = onRawMessage(msg => { // Handle remote console logs specially if (isRemoteConsoleLog(msg)) { @@ -233,6 +233,7 @@ export class Client implements IChannelClient, IDisposable { this.child.on('exit', (code: any, signal: any) => { process.removeListener('exit' as 'loaded', onExit); // https://github.com/electron/electron/issues/21475 + rawMessageDisposable.dispose(); this.activeRequests.forEach(r => dispose(r)); this.activeRequests.clear(); diff --git a/code/src/vs/base/test/browser/highlightedLabel.test.ts b/code/src/vs/base/test/browser/highlightedLabel.test.ts index 4f5eb5ca015..fe2ceb43d61 100644 --- a/code/src/vs/base/test/browser/highlightedLabel.test.ts +++ b/code/src/vs/base/test/browser/highlightedLabel.test.ts @@ -61,5 +61,9 @@ suite('HighlightedLabel', () => { assert.deepStrictEqual(highlights, [{ start: 5, end: 8 }, { start: 10, end: 11 }]); }); + teardown(() => { + label.dispose(); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/code/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts b/code/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts index 14de7bb4599..2ea6a9c1df9 100644 --- a/code/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts +++ b/code/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts @@ -53,7 +53,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, false); + assert.strictEqual(actual, false, `i = ${i}`); } }); @@ -142,7 +142,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, false); + assert.strictEqual(actual, false, `i = ${i}`); } }); @@ -202,7 +202,8 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, true); + assert.strictEqual(actual, true, `i = ${i}`); + } }); @@ -241,7 +242,8 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, true); + assert.strictEqual(actual, true, `i = ${i}`); + } }); @@ -285,7 +287,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, false); + assert.strictEqual(actual, false, `i = ${i}`); } }); @@ -374,7 +376,8 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, true); + assert.strictEqual(actual, true, `i = ${i}`); + } }); @@ -464,7 +467,8 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, true); + assert.strictEqual(actual, true, `i = ${i}`); + } }); @@ -518,7 +522,208 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.strictEqual(actual, true); + assert.strictEqual(actual, true, `i = ${i}`); + } + }); + + test('Linux Wayland - Logitech G Pro Wireless', () => { + const testData: IMouseWheelEvent[] = [ + [1707837460397, -1.5, 0], + [1707837460449, -1.5, 0], + [1707837460498, -1.5, 0], + [1707837460553, -1.5, 0], + [1707837460574, -1.5, 0], + [1707837460602, -1.5, 0], + [1707837460623, -1.5, 0], + [1707837460643, -1.5, 0], + [1707837460664, -1.5, 0], + [1707837460685, -1.5, 0], + [1707837460713, -1.5, 0], + [1707837460762, -1.5, 0], + [1707837460978, 1.5, 0], + [1707837460998, 1.5, 0], + [1707837461012, 1.5, 0], + [1707837461025, 1.5, 0], + [1707837461032, 1.5, 0], + [1707837461046, 1.5, 0], + [1707837461067, 1.5, 0], + [1707837461081, 1.5, 0], + [1707837461095, 1.5, 0], + [1707837461123, 1.5, 0], + [1707837461157, 1.5, 0], + [1707837461219, 1.5, 0], + [1707837461288, -1.5, 0], + [1707837461324, -1.5, 0], + [1707837461338, -1.5, 0], + [1707837461352, -1.5, 0], + [1707837461366, -1.5, 0], + [1707837461373, -1.5, 0], + [1707837461387, -1.5, 0], + [1707837461394, -1.5, 0], + [1707837461400, -1.5, 0], + [1707837461407, -1.5, 0], + [1707837461414, -1.5, 0], + [1707837461442, -1.5, 0], + [1707837461525, 1.5, 0], + [1707837461532, 1.5, 0], + [1707837461539, 1.5, 0], + [1707837461546, 1.5, 0], + [1707837461553, 1.5, 0], + [1707837461560, 1.5, 0], + [1707837461567, 1.5, 0], + [1707837461574, 1.5, 0], + [1707837461581, 1.5, 0], + [1707837461664, -1.5, 0], + [1707837461678, -1.5, 0], + [1707837461685, -1.5, 0], + [1707837461692, -1.5, 0], + [1707837461699, -1.5, 0], + [1707837461706, -1.5, 0], + [1707837461713, -1.5, 0], + [1707837461720, -1.5, 0], + [1707837461727, -1.5, 0], + [1707837461803, 1.5, 0], + [1707837461810, 1.5, 0], + [1707837461817, 1.5, 0], + [1707837461824, 1.5, 0], + [1707837461831, 1.5, 0], + [1707837461838, 1.5, 0], + [1707837461845, 1.5, 0], + [1707837461852, 3, 0], + [1707837461873, 1.5, 0], + [1707837461942, -1.5, 0], + [1707837461949, -1.5, 0], + [1707837461956, -1.5, 0], + [1707837461963, -1.5, 0], + [1707837461970, -1.5, 0], + [1707837461977, -3, 0], + [1707837461984, -1.5, 0], + [1707837461991, -1.5, 0], + [1707837462081, 1.5, 0], + [1707837462088, 1.5, 0], + [1707837462241, -1.5, 0], + [1707837462253, -1.5, 0], + [1707837462256, -1.5, 0], + [1707837462262, -1.5, 0], + [1707837462268, -1.5, 0], + [1707837462276, -1.5, 0], + [1707837462282, -4.5, 0], + [1707837462292, -3, 0], + [1707837462300, -1.5, 0], + [1707837462485, -1.5, 0], + [1707837462492, -1.5, 0], + [1707837462498, -1.5, 0], + [1707837462505, -1.5, 0], + [1707837462511, -1.5, 0], + [1707837462518, -3, 0], + [1707837462525, -3, 0], + [1707837462532, -1.5, 0], + [1707837462741, -1.5, 0], + [1707837462755, -1.5, 0], + [1707837462761, -1.5, 0], + [1707837462768, -1.5, 0], + [1707837462775, -1.5, 0], + [1707837462909, 1.5, 0], + [1707837462921, 1.5, 0], + [1707837462928, 1.5, 0], + [1707837462935, 3, 0], + [1707837462942, 3, 0], + [1707837462949, 1.5, 0], + [1707837462956, 1.5, 0], + [1707837462963, 1.5, 0], + [1707837462970, 1.5, 0], + [1707837463180, 1.5, 0], + [1707837463188, 1.5, 0], + [1707837463194, 1.5, 0], + [1707837463199, 1.5, 0], + [1707837463206, 1.5, 0], + [1707837463213, 1.5, 0], + [1707837463220, 1.5, 0], + [1707837463227, 1.5, 0], + [1707837463234, 1.5, 0], + [1707837463241, 1.5, 0], + [1707837463426, 1.5, 0], + [1707837463434, 1.5, 0], + [1707837463440, 1.5, 0], + [1707837463446, 1.5, 0], + [1707837463451, 1.5, 0], + [1707837463456, 1.5, 0], + [1707837463463, 1.5, 0], + [1707837463470, 1.5, 0], + [1707837463477, 1.5, 0], + [1707837463766, 1.5, 0], + [1707837463774, 1.5, 0], + [1707837463781, 1.5, 0], + [1707837463786, 1.5, 0], + [1707837463792, 1.5, 0], + [1707837463797, 1.5, 0], + [1707837463804, 1.5, 0], + [1707837463817, 1.5, 0], + [1707837463940, -1.5, 0], + [1707837463956, -1.5, 0], + [1707837463963, -1.5, 0], + [1707837463977, -1.5, 0], + [1707837463984, -1.5, 0], + [1707837463991, -3, 0], + [1707837463998, -1.5, 0], + [1707837464005, -1.5, 0], + [1707837464185, -1.5, 0], + [1707837464192, -1.5, 0], + [1707837464199, -1.5, 0], + [1707837464206, -1.5, 0], + [1707837464213, -1.5, 0], + [1707837464220, -3, 0], + [1707837464227, -1.5, 0], + [1707837464392, -1.5, 0], + [1707837464399, -1.5, 0], + [1707837464405, -1.5, 0], + [1707837464409, -1.5, 0], + [1707837464414, -1.5, 0], + [1707837464421, -1.5, 0], + [1707837464430, -1.5, 0], + [1707837464577, 1.5, 0], + [1707837464588, 1.5, 0], + [1707837464595, 1.5, 0], + [1707837464602, 1.5, 0], + [1707837464609, 1.5, 0], + [1707837464616, 1.5, 0], + [1707837464623, 3, 0], + [1707837464630, 1.5, 0], + [1707837464637, 1.5, 0], + [1707837464838, 1.5, 0], + [1707837464845, 1.5, 0], + [1707837464852, 1.5, 0], + [1707837464859, 1.5, 0], + [1707837464866, 3, 0], + [1707837464872, 1.5, 0], + [1707837464879, 1.5, 0], + [1707837464886, 1.5, 0], + [1707837464893, 1.5, 0], + [1707837465084, 1.5, 0], + [1707837465091, 1.5, 0], + [1707837465097, 1.5, 0], + [1707837465102, 1.5, 0], + [1707837465109, 1.5, 0], + [1707837465116, 1.5, 0], + [1707837465122, 1.5, 0], + [1707837465129, 1.5, 0], + [1707837465136, 1.5, 0], + [1707837465157, 1.5, 0], + ]; + + const classifier = new MouseWheelClassifier(); + for (let i = 0, len = testData.length; i < len; i++) { + const [timestamp, deltaY, deltaX] = testData[i]; + classifier.accept(timestamp, deltaX, deltaY); + + const actual = classifier.isPhysicalMouseWheel(); + + // Linux Wayland implementation depends on looking at the + // previous event. + if (i > 0) { + assert.strictEqual(actual, true, `i = ${i}`); + } } }); + }); diff --git a/code/src/vs/base/test/common/strings.test.ts b/code/src/vs/base/test/common/strings.test.ts index 766380ac8ec..73c04ad7239 100644 --- a/code/src/vs/base/test/common/strings.test.ts +++ b/code/src/vs/base/test/common/strings.test.ts @@ -528,6 +528,14 @@ suite('Strings', () => { for (const sequence of sequences) { assert.strictEqual(strings.removeAnsiEscapeCodes(`hello${sequence}world`), 'helloworld', `expect to remove ${JSON.stringify(sequence)}`); } + + for (const sequence of sequences) { + assert.deepStrictEqual( + [...strings.forAnsiStringParts(`hello${sequence}world`)], + [{ isCode: false, str: 'hello' }, { isCode: true, str: sequence }, { isCode: false, str: 'world' }], + `expect to forAnsiStringParts ${JSON.stringify(sequence)}` + ); + } }); ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/code/src/vs/code/electron-main/app.ts b/code/src/vs/code/electron-main/app.ts index 7a039050402..185616b5696 100644 --- a/code/src/vs/code/electron-main/app.ts +++ b/code/src/vs/code/electron-main/app.ts @@ -193,7 +193,7 @@ export class CodeApplication extends Disposable { // Block all SVG requests from unsupported origins const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, Schemas.vscodeManagedRemoteResource, 'devtools']); - // But allow them if the are made from inside an webview + // But allow them if they are made from inside an webview const isSafeFrame = (requestFrame: WebFrameMain | undefined): boolean => { for (let frame: WebFrameMain | null | undefined = requestFrame; frame; frame = frame.parent) { if (frame.url.startsWith(`${Schemas.vscodeWebview}://`)) { diff --git a/code/src/vs/code/electron-main/main.ts b/code/src/vs/code/electron-main/main.ts index c5466e20b45..8548fa7ce4d 100644 --- a/code/src/vs/code/electron-main/main.ts +++ b/code/src/vs/code/electron-main/main.ts @@ -71,6 +71,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { SaveStrategy, StateService } from 'vs/platform/state/node/stateService'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; /** * The main VS Code entry point. @@ -249,8 +250,8 @@ class CodeMain { // Environment service (paths) Promise.all([ - environmentMainService.extensionsPath, - environmentMainService.codeCachePath, + this.allowWindowsUNCPath(environmentMainService.extensionsPath), // enable extension paths on UNC drives... + environmentMainService.codeCachePath, // ...other user-data-derived paths should already be enlisted from `main.js` environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, userDataProfilesMainService.defaultProfile.globalStorageHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, @@ -269,6 +270,17 @@ class CodeMain { userDataProfilesMainService.init(); } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means diff --git a/code/src/vs/code/electron-sandbox/issue/issueReporterModel.ts b/code/src/vs/code/electron-sandbox/issue/issueReporterModel.ts index 74f993903e1..1541f98c812 100644 --- a/code/src/vs/code/electron-sandbox/issue/issueReporterModel.ts +++ b/code/src/vs/code/electron-sandbox/issue/issueReporterModel.ts @@ -29,6 +29,7 @@ export interface IssueReporterData { extensionsDisabled?: boolean; fileOnExtension?: boolean; fileOnMarketplace?: boolean; + fileOnProduct?: boolean; selectedExtension?: IssueReporterExtensionData; actualSearchResults?: ISettingSearchResult[]; query?: string; diff --git a/code/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/code/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 067f09fd0bd..0887c6cc47d 100644 --- a/code/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/code/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -74,10 +74,18 @@ export class IssueReporter extends Disposable { selectedExtension: targetExtension }); + const fileOnMarketplace = configuration.data.issueSource === IssueSource.Marketplace; + const fileOnProduct = configuration.data.issueSource === IssueSource.VSCode; + this.issueReporterModel.update({ fileOnMarketplace, fileOnProduct }); + //TODO: Handle case where extension is not activated const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { this.previewButton = new Button(issueReporterElement, unthemedButtonStyles); + const issueRepoName = document.createElement('a'); + issueReporterElement.appendChild(issueRepoName); + issueRepoName.id = 'show-repo-name'; + issueRepoName.classList.add('hidden'); this.updatePreviewButtonState(); } @@ -119,7 +127,7 @@ export class IssueReporter extends Disposable { codiconStyleSheet.id = 'codiconStyles'; // TODO: Is there a way to use the IThemeService here instead - const iconsStyleSheet = getIconsStyleSheet(undefined); + const iconsStyleSheet = this._register(getIconsStyleSheet(undefined)); function updateAll() { codiconStyleSheet.textContent = iconsStyleSheet.getCSS(); } @@ -501,6 +509,31 @@ export class IssueReporter extends Disposable { this.previewButton.enabled = false; this.previewButton.label = localize('loadingData', "Loading data..."); } + + const issueRepoName = this.getElementById('show-repo-name')! as HTMLAnchorElement; + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension && selectedExtension.uri) { + const urlString = URI.revive(selectedExtension.uri).toString(); + issueRepoName.href = urlString; + issueRepoName.addEventListener('click', (e) => this.openLink(e)); + issueRepoName.addEventListener('auxclick', (e) => this.openLink(e)); + const gitHubInfo = this.parseGitHubUrl(urlString); + issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString; + Object.assign(issueRepoName.style, { + alignSelf: 'flex-end', + display: 'block', + fontSize: '13px', + marginBottom: '10px', + padding: '4px 0px', + textDecoration: 'none', + width: 'auto' + }); + show(issueRepoName); + } else { + // clear styles + issueRepoName.removeAttribute('style'); + hide(issueRepoName); + } } private isPreviewEnabled() { @@ -743,13 +776,17 @@ export class IssueReporter extends Disposable { private setSourceOptions(): void { const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement; - const { issueType, fileOnExtension, selectedExtension } = this.issueReporterModel.getData(); + const { issueType, fileOnExtension, selectedExtension, fileOnMarketplace, fileOnProduct } = this.issueReporterModel.getData(); let selected = sourceSelect.selectedIndex; if (selected === -1) { if (fileOnExtension !== undefined) { selected = fileOnExtension ? 2 : 1; } else if (selectedExtension?.isBuiltin) { selected = 1; + } else if (fileOnMarketplace) { + selected = 3; + } else if (fileOnProduct) { + selected = 1; } } diff --git a/code/src/vs/code/node/cliProcessMain.ts b/code/src/vs/code/node/cliProcessMain.ts index b91367f1fc2..aea83578ee0 100644 --- a/code/src/vs/code/node/cliProcessMain.ts +++ b/code/src/vs/code/node/cliProcessMain.ts @@ -63,6 +63,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { localize } from 'vs/nls'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; class CliMain extends Disposable { @@ -121,8 +122,8 @@ class CliMain extends Disposable { // Init folders await Promise.all([ - environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath, - environmentService.extensionsPath + this.allowWindowsUNCPath(environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath), + this.allowWindowsUNCPath(environmentService.extensionsPath) ].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Logger @@ -233,6 +234,17 @@ class CliMain extends Disposable { return [new InstantiationService(services), appenders]; } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private registerErrorHandler(logService: ILogService): void { // Install handler for unexpected errors diff --git a/code/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/code/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 3c0de38db43..81faf87e87b 100644 --- a/code/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/code/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -519,15 +519,24 @@ export async function main(configuration: ISharedProcessConfiguration): Promise< // create shared process and signal back to main that we are // ready to accept message ports as client connections - const sharedProcess = new SharedProcessMain(configuration); - process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); + try { + const sharedProcess = new SharedProcessMain(configuration); + process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); - // await initialization and signal this back to electron-main - await sharedProcess.init(); + // await initialization and signal this back to electron-main + await sharedProcess.init(); - process.parentPort.postMessage(SharedProcessLifecycle.initDone); + process.parentPort.postMessage(SharedProcessLifecycle.initDone); + } catch (error) { + process.parentPort.postMessage({ error: error.toString() }); + } } +const handle = setTimeout(() => { + process.parentPort.postMessage({ warning: '[SharedProcess] did not receive configuration within 30s...' }); +}, 30000); + process.parentPort.once('message', (e: Electron.MessageEvent) => { + clearTimeout(handle); main(e.data as ISharedProcessConfiguration); }); diff --git a/code/src/vs/editor/browser/controller/textAreaHandler.ts b/code/src/vs/editor/browser/controller/textAreaHandler.ts index f1661da1fc4..c8e5b7e50a4 100644 --- a/code/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/code/src/vs/editor/browser/controller/textAreaHandler.ts @@ -490,7 +490,7 @@ export class TextAreaHandler extends ViewPart { private _getAndroidWordAtPosition(position: Position): [string, number] { const ANDROID_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:",.<>/?'; const lineContent = this._context.viewModel.getLineContent(position.lineNumber); - const wordSeparators = getMapForWordSeparators(ANDROID_WORD_SEPARATORS); + const wordSeparators = getMapForWordSeparators(ANDROID_WORD_SEPARATORS, []); let goingLeft = true; let startColumn = position.column; @@ -530,7 +530,7 @@ export class TextAreaHandler extends ViewPart { private _getWordBeforePosition(position: Position): string { const lineContent = this._context.viewModel.getLineContent(position.lineNumber); - const wordSeparators = getMapForWordSeparators(this._context.configuration.options.get(EditorOption.wordSeparators)); + const wordSeparators = getMapForWordSeparators(this._context.configuration.options.get(EditorOption.wordSeparators), []); let column = position.column; let distance = 0; diff --git a/code/src/vs/editor/browser/editorBrowser.ts b/code/src/vs/editor/browser/editorBrowser.ts index ecedcdb42a9..7678c6e3b88 100644 --- a/code/src/vs/editor/browser/editorBrowser.ts +++ b/code/src/vs/editor/browser/editorBrowser.ts @@ -510,6 +510,18 @@ export interface IPartialEditorMouseEvent { export interface IPasteEvent { readonly range: Range; readonly languageId: string | null; + readonly clipboardEvent?: ClipboardEvent; +} + +/** + * @internal + */ +export interface PastePayload { + text: string; + pasteOnNewLine: boolean; + multicursorText: string[] | null; + mode: string | null; + clipboardEvent?: ClipboardEvent; } /** diff --git a/code/src/vs/editor/browser/services/editorWorkerService.ts b/code/src/vs/editor/browser/services/editorWorkerService.ts index f707c6fa25e..aa1e41c8985 100644 --- a/code/src/vs/editor/browser/services/editorWorkerService.ts +++ b/code/src/vs/editor/browser/services/editorWorkerService.ts @@ -29,7 +29,8 @@ import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/di import { ILinesDiffComputerOptions, MovedText } from 'vs/editor/common/diff/linesDiffComputer'; import { DetailedLineRangeMapping, RangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { LineRange } from 'vs/editor/common/core/lineRange'; -import { $window } from 'vs/base/browser/window'; +import { SectionHeader, FindSectionHeaderOptions } from 'vs/editor/common/services/findSectionHeaders'; +import { mainWindow } from 'vs/base/browser/window'; import { WindowIntervalTimer } from 'vs/base/browser/dom'; /** @@ -190,6 +191,10 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> { return this._workerManager.withWorker().then(client => client.computeWordRanges(resource, range)); } + + public findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise { + return this._workerManager.withWorker().then(client => client.findSectionHeaders(uri, options)); + } } class WordBasedCompletionItemProvider implements languages.CompletionItemProvider { @@ -283,7 +288,7 @@ class WorkerManager extends Disposable { this._lastWorkerUsedTime = (new Date()).getTime(); const stopWorkerInterval = this._register(new WindowIntervalTimer()); - stopWorkerInterval.cancelAndSet(() => this._checkStopIdleWorker(), Math.round(STOP_WORKER_DELTA_TIME_MS / 2), $window); + stopWorkerInterval.cancelAndSet(() => this._checkStopIdleWorker(), Math.round(STOP_WORKER_DELTA_TIME_MS / 2), mainWindow); this._register(this._modelService.onModelRemoved(_ => this._checkStopEmptyWorker())); } @@ -613,6 +618,12 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien }); } + public findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise { + return this._withSyncedResources([uri]).then(proxy => { + return proxy.findSectionHeaders(uri.toString(), options); + }); + } + override dispose(): void { super.dispose(); this._disposed = true; diff --git a/code/src/vs/editor/browser/services/hoverService/hoverService.ts b/code/src/vs/editor/browser/services/hoverService/hoverService.ts index f7338ae7b52..38357608b86 100644 --- a/code/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/code/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -7,11 +7,11 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions } from 'vs/platform/hover/browser/hover'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { HoverWidget } from 'vs/editor/browser/services/hoverService/hoverWidget'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow } from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -19,11 +19,13 @@ import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { mainWindow } from 'vs/base/browser/window'; -import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { ContextViewHandler } from 'vs/platform/contextview/browser/contextViewService'; -export class HoverService implements IHoverService { +export class HoverService extends Disposable implements IHoverService { declare readonly _serviceBrand: undefined; + private _contextViewHandler: IContextViewProvider; private _currentHoverOptions: IHoverOptions | undefined; private _currentHover: HoverWidget | undefined; private _lastHoverOptions: IHoverOptions | undefined; @@ -32,13 +34,15 @@ export class HoverService implements IHoverService { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IContextViewService private readonly _contextViewService: IContextViewService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ILayoutService private readonly _layoutService: ILayoutService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { + super(); + contextMenuService.onDidShowContextMenu(() => this.hideHover()); + this._contextViewHandler = this._register(new ContextViewHandler(this._layoutService)); } showHover(options: IHoverOptions, focus?: boolean, skipLastFocusedUpdate?: boolean): IHoverWidget | undefined { @@ -84,12 +88,12 @@ export class HoverService implements IHoverService { const targetElement = options.target instanceof HTMLElement ? options.target : options.target.targetElements[0]; options.container = this._layoutService.getContainer(getWindow(targetElement)); } - const provider = this._contextViewService as IContextViewProvider; - provider.showContextView( + + this._contextViewHandler.showContextView( new HoverContextViewDelegate(hover, focus), options.container ); - hover.onRequestLayout(() => provider.layout()); + hover.onRequestLayout(() => this._contextViewHandler.layout()); if (options.persistence?.sticky) { hoverDisposables.add(addDisposableListener(getWindow(options.container).document, EventType.MOUSE_DOWN, e => { if (!isAncestor(e.target as HTMLElement, hover.domNode)) { @@ -136,7 +140,7 @@ export class HoverService implements IHoverService { private doHideHover(): void { this._currentHover = undefined; this._currentHoverOptions = undefined; - this._contextViewService.hideContextView(); + this._contextViewHandler.hideContextView(); } private _intersectionChange(entries: IntersectionObserverEntry[], hover: IDisposable): void { @@ -190,6 +194,9 @@ function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOpti class HoverContextViewDelegate implements IDelegate { + // Render over all other context views + public readonly layer = 1; + get anchorPosition() { return this._hover.anchor; } diff --git a/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts index 8b4814a92c9..24ae9cf483e 100644 --- a/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -23,7 +23,7 @@ import { localize } from 'vs/nls'; import { isMacintosh } from 'vs/base/common/platform'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { status } from 'vs/base/browser/ui/aria/aria'; -import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; const $ = dom.$; type TargetRect = { @@ -469,9 +469,11 @@ export class HoverWidget extends Widget implements IHoverWidget { return; } + const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0); + // When force position is enabled, restrict max width if (this._forcePosition) { - const padding = (this._hoverPointer ? Constants.PointerSize : 0) + Constants.HoverBorderWidth; + const padding = hoverPointerOffset + Constants.HoverBorderWidth; if (this._hoverPosition === HoverPosition.RIGHT) { this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`; } else if (this._hoverPosition === HoverPosition.LEFT) { @@ -484,10 +486,10 @@ export class HoverWidget extends Widget implements IHoverWidget { if (this._hoverPosition === HoverPosition.RIGHT) { const roomOnRight = this._targetDocumentElement.clientWidth - target.right; // Hover on the right is going beyond window. - if (roomOnRight < this._hover.containerDomNode.clientWidth) { + if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) { const roomOnLeft = target.left; // There's enough room on the left, flip the hover position - if (roomOnLeft >= this._hover.containerDomNode.clientWidth) { + if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) { this._hoverPosition = HoverPosition.LEFT; } // Hover on the left would go beyond window too @@ -501,10 +503,10 @@ export class HoverWidget extends Widget implements IHoverWidget { const roomOnLeft = target.left; // Hover on the left is going beyond window. - if (roomOnLeft < this._hover.containerDomNode.clientWidth) { + if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) { const roomOnRight = this._targetDocumentElement.clientWidth - target.right; // There's enough room on the right, flip the hover position - if (roomOnRight >= this._hover.containerDomNode.clientWidth) { + if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) { this._hoverPosition = HoverPosition.RIGHT; } // Hover on the right would go beyond window too @@ -513,7 +515,7 @@ export class HoverWidget extends Widget implements IHoverWidget { } } // Hover on the left is going beyond window. - if (target.left - this._hover.containerDomNode.clientWidth <= this._targetDocumentElement.clientLeft) { + if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) { this._hoverPosition = HoverPosition.RIGHT; } } @@ -526,10 +528,12 @@ export class HoverWidget extends Widget implements IHoverWidget { return; } + const hoverPointerOffset = (this._hoverPointer ? Constants.PointerSize : 0); + // Position hover on top of the target if (this._hoverPosition === HoverPosition.ABOVE) { // Hover on top is going beyond window - if (target.top - this._hover.containerDomNode.clientHeight < 0) { + if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) { this._hoverPosition = HoverPosition.BELOW; } } @@ -537,7 +541,7 @@ export class HoverWidget extends Widget implements IHoverWidget { // Position hover below the target else if (this._hoverPosition === HoverPosition.BELOW) { // Hover on bottom is going beyond window - if (target.bottom + this._hover.containerDomNode.clientHeight > this._targetWindow.innerHeight) { + if (target.bottom + this._hover.containerDomNode.clientHeight + hoverPointerOffset > this._targetWindow.innerHeight) { this._hoverPosition = HoverPosition.ABOVE; } } diff --git a/code/src/vs/editor/browser/view.ts b/code/src/vs/editor/browser/view.ts index ba0b5008760..a803234c4b4 100644 --- a/code/src/vs/editor/browser/view.ts +++ b/code/src/vs/editor/browser/view.ts @@ -339,8 +339,9 @@ export class View extends ViewEventHandler { this._overflowGuardContainer.setWidth(layoutInfo.width); this._overflowGuardContainer.setHeight(layoutInfo.height); - this._linesContent.setWidth(1000000); - this._linesContent.setHeight(1000000); + // https://stackoverflow.com/questions/38905916/content-in-google-chrome-larger-than-16777216-px-not-being-rendered + this._linesContent.setWidth(16777216); + this._linesContent.setHeight(16777216); } private _getEditorClassName() { diff --git a/code/src/vs/editor/browser/view/viewLayer.ts b/code/src/vs/editor/browser/view/viewLayer.ts index c15239ec8b1..bbbb0dd9d73 100644 --- a/code/src/vs/editor/browser/view/viewLayer.ts +++ b/code/src/vs/editor/browser/view/viewLayer.ts @@ -22,12 +22,12 @@ export interface IVisibleLine extends ILine { * Return null if the HTML should not be touched. * Return the new HTML otherwise. */ - renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean; + renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean; /** * Layout the line. */ - layoutLine(lineNumber: number, deltaTop: number): void; + layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void; } export interface ILine { @@ -465,7 +465,7 @@ class ViewLayerRenderer { for (let i = startIndex; i <= endIndex; i++) { const lineNumber = rendLineNumberStart + i; - lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN]); + lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this.viewportData.lineHeight); } } @@ -573,7 +573,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -603,7 +603,7 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb); + const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData.lineHeight, this.viewportData, sb); if (!renderResult) { // line does not need rendering continue; diff --git a/code/src/vs/editor/browser/view/viewOverlays.ts b/code/src/vs/editor/browser/view/viewOverlays.ts index 83a3cc05d6f..1041fd58a58 100644 --- a/code/src/vs/editor/browser/view/viewOverlays.ts +++ b/code/src/vs/editor/browser/view/viewOverlays.ts @@ -9,7 +9,6 @@ import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; import { IVisibleLine, IVisibleLinesHost, VisibleLinesCollection } from 'vs/editor/browser/view/viewLayer'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; @@ -71,7 +70,7 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost | null; private _renderedContent: string | null; - private _lineHeight: number; - constructor(configuration: IEditorConfiguration, dynamicOverlays: DynamicViewOverlay[]) { - this._configuration = configuration; - this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); + constructor(dynamicOverlays: DynamicViewOverlay[]) { this._dynamicOverlays = dynamicOverlays; this._domNode = null; @@ -180,11 +169,8 @@ export class ViewOverlayLine implements IVisibleLine { public onTokensChanged(): void { // Nothing } - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void { - this._lineHeight = this._configuration.options.get(EditorOption.lineHeight); - } - public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { let result = ''; for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) { const dynamicOverlay = this._dynamicOverlays[i]; @@ -198,10 +184,10 @@ export class ViewOverlayLine implements IVisibleLine { this._renderedContent = result; - sb.appendString('
'); sb.appendString(result); sb.appendString('
'); @@ -209,10 +195,10 @@ export class ViewOverlayLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number): void { + public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { if (this._domNode) { this._domNode.setTop(deltaTop); - this._domNode.setHeight(this._lineHeight); + this._domNode.setHeight(lineHeight); } } } diff --git a/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css b/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css index 2a0e39dffa7..403e255fac8 100644 --- a/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css +++ b/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css @@ -9,6 +9,7 @@ left: 0; top: 0; box-sizing: border-box; + height: 100%; } .monaco-editor .margin-view-overlays .current-line { @@ -17,8 +18,11 @@ left: 0; top: 0; box-sizing: border-box; + height: 100%; } -.monaco-editor .margin-view-overlays .current-line.current-line-margin.current-line-margin-both { +.monaco-editor + .margin-view-overlays + .current-line.current-line-margin.current-line-margin-both { border-right: 0; } diff --git a/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 64649e0b835..b35970ee373 100644 --- a/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/code/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -18,7 +18,6 @@ import { Position } from 'vs/editor/common/core/position'; export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; - protected _lineHeight: number; protected _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all'; protected _wordWrap: boolean; protected _contentLeft: number; @@ -39,7 +38,6 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -89,7 +87,6 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._renderLineHighlight = options.get(EditorOption.renderLineHighlight); this._renderLineHighlightOnlyWhenFocus = options.get(EditorOption.renderLineHighlightOnlyWhenFocus); this._wordWrap = layoutInfo.isViewportWrapping; @@ -208,7 +205,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-both' : '') + (exact ? ' current-line-exact' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return this._shouldRenderInContent(); @@ -221,7 +218,7 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-margin' : '') + (this._shouldRenderOther() ? ' current-line-margin-both' : '') + (this._shouldRenderInMargin() && exact ? ' current-line-exact-margin' : ''); - return `
`; + return `
`; } protected _shouldRenderThis(): boolean { return true; diff --git a/code/src/vs/editor/browser/viewParts/decorations/decorations.css b/code/src/vs/editor/browser/viewParts/decorations/decorations.css index 37c39f620e8..4c755e2dbf8 100644 --- a/code/src/vs/editor/browser/viewParts/decorations/decorations.css +++ b/code/src/vs/editor/browser/viewParts/decorations/decorations.css @@ -9,4 +9,5 @@ */ .monaco-editor .lines-content .cdr { position: absolute; -} \ No newline at end of file + height: 100%; +} diff --git a/code/src/vs/editor/browser/viewParts/decorations/decorations.ts b/code/src/vs/editor/browser/viewParts/decorations/decorations.ts index fe495466b1d..a3baa510464 100644 --- a/code/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/code/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -15,7 +15,6 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; export class DecorationsOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; - private _lineHeight: number; private _typicalHalfwidthCharacterWidth: number; private _renderResult: string[] | null; @@ -23,7 +22,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._renderResult = null; @@ -40,7 +38,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; } @@ -116,7 +113,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderWholeLineDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { - const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; @@ -130,9 +126,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { const decorationOutput = ( '
' + + '" style="left:0;width:100%;">' ); const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber); @@ -145,7 +139,6 @@ export class DecorationsOverlay extends DynamicViewOverlay { } private _renderNormalDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void { - const lineHeight = String(this._lineHeight); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; let prevClassName: string | null = null; @@ -176,7 +169,7 @@ export class DecorationsOverlay extends DynamicViewOverlay { // flush previous decoration if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); } prevClassName = className; @@ -186,11 +179,11 @@ export class DecorationsOverlay extends DynamicViewOverlay { } if (prevClassName !== null) { - this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, lineHeight, visibleStartLineNumber, output); + this._renderNormalDecoration(ctx, prevRange!, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output); } } - private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, lineHeight: string, visibleStartLineNumber: number, output: string[]): void { + private _renderNormalDecoration(ctx: RenderingContext, range: Range, className: string, shouldFillLineOnLineBreak: boolean, showIfCollapsed: boolean, visibleStartLineNumber: number, output: string[]): void { const linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch'); if (!linesVisibleRanges) { return; @@ -222,12 +215,12 @@ export class DecorationsOverlay extends DynamicViewOverlay { + className + '" style="left:' + String(visibleRange.left) + + 'px;width:' + (expandToLeft ? - 'px;width:100%;height:' : - ('px;width:' + String(visibleRange.width) + 'px;height:') + '100%;' : + (String(visibleRange.width) + 'px;') ) - + lineHeight - + 'px;">' + + '">' ); output[lineIndex] += decorationOutput; } diff --git a/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css b/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css index ed132669757..6aacf7c2126 100644 --- a/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css +++ b/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css @@ -6,4 +6,5 @@ .monaco-editor .lines-content .core-guide { position: absolute; box-sizing: border-box; + height: 100%; } diff --git a/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index a93cf75a530..50b0b2b8661 100644 --- a/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/code/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -22,7 +22,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; private _primaryPosition: Position | null; - private _lineHeight: number; private _spaceWidth: number; private _renderResult: string[] | null; private _maxIndentLeft: number; @@ -37,7 +36,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -60,7 +58,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); this._bracketPairGuideOptions = options.get(EditorOption.guides); @@ -114,7 +111,6 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const scrollWidth = ctx.scrollWidth; - const lineHeight = this._lineHeight; const activeCursorPosition = this._primaryPosition; @@ -150,7 +146,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { )?.left ?? (left + this._spaceWidth)) - left : this._spaceWidth; - result += `
`; + result += `
`; } output[lineIndex] = result; } diff --git a/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css b/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css index 774ffef273d..2961137b032 100644 --- a/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css +++ b/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .margin-view-overlays .line-numbers { + bottom: 0; font-variant-numeric: tabular-nums; position: absolute; text-align: right; @@ -11,7 +12,6 @@ vertical-align: middle; box-sizing: border-box; cursor: default; - height: 100%; } .monaco-editor .relative-current-line-number { diff --git a/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index 03336d34278..dcebb27994e 100644 --- a/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/code/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts @@ -131,6 +131,10 @@ export class LineNumbersOverlay extends DynamicViewOverlay { if (modelLineNumber % 10 === 0) { return String(modelLineNumber); } + const finalLineNumber = this._context.viewModel.getLineCount(); + if (modelLineNumber === finalLineNumber) { + return String(modelLineNumber); + } return ''; } diff --git a/code/src/vs/editor/browser/viewParts/lines/viewLine.ts b/code/src/vs/editor/browser/viewParts/lines/viewLine.ts index e4174a2f286..9a5d2f556bf 100644 --- a/code/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/code/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -151,7 +151,7 @@ export class ViewLine implements IVisibleLine { return false; } - public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: StringBuilder): boolean { + public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { if (this._isMaybeInvalid === false) { // it appears that nothing relevant has changed return false; @@ -222,7 +222,7 @@ export class ViewLine implements IVisibleLine { sb.appendString('
'); @@ -255,10 +255,10 @@ export class ViewLine implements IVisibleLine { return true; } - public layoutLine(lineNumber: number, deltaTop: number): void { + public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void { if (this._renderedViewLine && this._renderedViewLine.domNode) { this._renderedViewLine.domNode.setTop(deltaTop); - this._renderedViewLine.domNode.setHeight(this._options.lineHeight); + this._renderedViewLine.domNode.setHeight(lineHeight); } } diff --git a/code/src/vs/editor/browser/viewParts/lines/viewLines.css b/code/src/vs/editor/browser/viewParts/lines/viewLines.css index fe686d3e441..e4e187c2be2 100644 --- a/code/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/code/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -63,6 +63,12 @@ width: 100%; } +/* There are view-lines in view-zones. We have to make sure this rule does not apply to them, as they don't set a line height */ +.monaco-editor .lines-content > .view-lines > .view-line > span { + bottom: 0; + position: absolute; +} + .monaco-editor .mtkw { color: var(--vscode-editorWhitespace-foreground) !important; } diff --git a/code/src/vs/editor/browser/viewParts/minimap/minimap.ts b/code/src/vs/editor/browser/viewParts/minimap/minimap.ts index 789b68836b9..427df7cd3d8 100644 --- a/code/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/code/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -27,14 +27,16 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { EditorTheme } from 'vs/editor/common/editorTheme'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewLineData, ViewModelDecoration } from 'vs/editor/common/viewModel'; -import { minimapSelection, minimapBackground, minimapForegroundOpacity } from 'vs/platform/theme/common/colorRegistry'; +import { minimapSelection, minimapBackground, minimapForegroundOpacity, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; import { Selection } from 'vs/editor/common/core/selection'; import { Color } from 'vs/base/common/color'; import { GestureEvent, EventType, Gesture } from 'vs/base/browser/touch'; import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory'; -import { MinimapPosition, TextModelResolvedOptions } from 'vs/editor/common/model'; +import { MinimapPosition, MinimapSectionHeaderStyle, TextModelResolvedOptions } from 'vs/editor/common/model'; import { createSingleCallFunction } from 'vs/base/common/functional'; +import { LRUCache } from 'vs/base/common/map'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; /** * The orthogonal distance to the slider at which dragging "resets". This implements "snapping" @@ -90,6 +92,9 @@ class MinimapOptions { public readonly fontScale: number; public readonly minimapLineHeight: number; public readonly minimapCharWidth: number; + public readonly sectionHeaderFontFamily: string; + public readonly sectionHeaderFontSize: number; + public readonly sectionHeaderFontColor: RGBA8; public readonly charRenderer: () => MinimapCharRenderer; public readonly defaultBackgroundColor: RGBA8; @@ -132,6 +137,9 @@ class MinimapOptions { this.fontScale = minimapLayout.minimapScale; this.minimapLineHeight = minimapLayout.minimapLineHeight; this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; + this.sectionHeaderFontFamily = DEFAULT_FONT_FAMILY; + this.sectionHeaderFontSize = minimapOpts.sectionHeaderFontSize * pixelRatio; + this.sectionHeaderFontColor = MinimapOptions._getSectionHeaderColor(theme, tokensColorTracker.getColor(ColorId.DefaultForeground)); this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.defaultBackgroundColor = tokensColorTracker.getColor(ColorId.DefaultBackground); @@ -155,6 +163,14 @@ class MinimapOptions { return 255; } + private static _getSectionHeaderColor(theme: EditorTheme, defaultForegroundColor: RGBA8): RGBA8 { + const themeColor = theme.getColor(editorForeground); + if (themeColor) { + return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a)); + } + return defaultForegroundColor; + } + public equals(other: MinimapOptions): boolean { return (this.renderMinimap === other.renderMinimap && this.size === other.size @@ -179,6 +195,7 @@ class MinimapOptions { && this.fontScale === other.fontScale && this.minimapLineHeight === other.minimapLineHeight && this.minimapCharWidth === other.minimapCharWidth + && this.sectionHeaderFontSize === other.sectionHeaderFontSize && this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor) && this.backgroundColor && this.backgroundColor.equals(other.backgroundColor) && this.foregroundAlpha === other.foregroundAlpha @@ -544,6 +561,8 @@ export interface IMinimapModel { getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[]; getSelections(): Selection[]; getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[]; + getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[]; + getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null; getOptions(): TextModelResolvedOptions; revealLineNumber(lineNumber: number): void; setScrollTop(scrollTop: number): void; @@ -697,7 +716,7 @@ class MinimapSamplingState { constructor( public readonly samplingRatio: number, - public readonly minimapLines: number[] + public readonly minimapLines: number[] // a map of 0-based minimap line indexes to 1-based view line numbers ) { } @@ -790,6 +809,8 @@ export class Minimap extends ViewPart implements IMinimapModel { private _samplingState: MinimapSamplingState | null; private _shouldCheckSampling: boolean; + private _sectionHeaderCache = new LRUCache(10, 1.5); + private _actual: InnerMinimap; constructor(context: ViewContext) { @@ -1037,15 +1058,8 @@ export class Minimap extends ViewPart implements IMinimapModel { } public getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] { - let visibleRange: Range; - if (this._samplingState) { - const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1]; - const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1]; - visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.viewModel.getLineMaxColumn(modelEndLineNumber)); - } else { - visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.viewModel.getLineMaxColumn(endLineNumber)); - } - const decorations = this._context.viewModel.getMinimapDecorationsInRange(visibleRange); + const decorations = this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber) + .filter(decoration => !decoration.options.minimap?.sectionHeaderStyle); if (this._samplingState) { const result: ViewModelDecoration[] = []; @@ -1063,6 +1077,41 @@ export class Minimap extends ViewPart implements IMinimapModel { return decorations; } + public getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] { + const minimapLineHeight = this.options.minimapLineHeight; + const sectionHeaderFontSize = this.options.sectionHeaderFontSize; + const headerHeightInMinimapLines = sectionHeaderFontSize / minimapLineHeight; + startLineNumber = Math.floor(Math.max(1, startLineNumber - headerHeightInMinimapLines)); + return this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber) + .filter(decoration => !!decoration.options.minimap?.sectionHeaderStyle); + } + + private _getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number) { + let visibleRange: Range; + if (this._samplingState) { + const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1]; + const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1]; + visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.viewModel.getLineMaxColumn(modelEndLineNumber)); + } else { + visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.viewModel.getLineMaxColumn(endLineNumber)); + } + return this._context.viewModel.getMinimapDecorationsInRange(visibleRange); + } + + public getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null { + const headerText = decoration.options.minimap?.sectionHeaderText; + if (!headerText) { + return null; + } + const cachedText = this._sectionHeaderCache.get(headerText); + if (cachedText) { + return cachedText; + } + const fittedText = fitWidth(headerText); + this._sectionHeaderCache.set(headerText, fittedText); + return fittedText; + } + public getOptions(): TextModelResolvedOptions { return this._context.viewModel.model.getOptions(); } @@ -1469,6 +1518,7 @@ class InnerMinimap extends Disposable { const lineOffsetMap = new ContiguousLineMap(layout.startLineNumber, layout.endLineNumber, null); this._renderSelectionsHighlights(canvasContext, selections, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth); this._renderDecorationsHighlights(canvasContext, decorations, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth); + this._renderSectionHeaders(layout); } } @@ -1735,6 +1785,110 @@ class InnerMinimap extends Disposable { canvasContext.fillRect(x, y, width, height); } + private _renderSectionHeaders(layout: MinimapLayout) { + const minimapLineHeight = this._model.options.minimapLineHeight; + const sectionHeaderFontSize = this._model.options.sectionHeaderFontSize; + const backgroundFillHeight = sectionHeaderFontSize * 1.5; + const { canvasInnerWidth } = this._model.options; + + const backgroundColor = this._model.options.backgroundColor; + const backgroundFill = `rgb(${backgroundColor.r} ${backgroundColor.g} ${backgroundColor.b} / .7)`; + const foregroundColor = this._model.options.sectionHeaderFontColor; + const foregroundFill = `rgb(${foregroundColor.r} ${foregroundColor.g} ${foregroundColor.b})`; + const separatorStroke = foregroundFill; + + const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!; + canvasContext.font = sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily; + canvasContext.strokeStyle = separatorStroke; + canvasContext.lineWidth = 0.2; + + const decorations = this._model.getSectionHeaderDecorationsInViewport(layout.startLineNumber, layout.endLineNumber); + decorations.sort((a, b) => a.range.startLineNumber - b.range.startLineNumber); + + const fitWidth = InnerMinimap._fitSectionHeader.bind(null, canvasContext, + canvasInnerWidth - MINIMAP_GUTTER_WIDTH); + + for (const decoration of decorations) { + const y = layout.getYForLineNumber(decoration.range.startLineNumber, minimapLineHeight) + sectionHeaderFontSize; + const backgroundFillY = y - sectionHeaderFontSize; + const separatorY = backgroundFillY + 2; + const headerText = this._model.getSectionHeaderText(decoration, fitWidth); + + InnerMinimap._renderSectionLabel( + canvasContext, + headerText, + decoration.options.minimap?.sectionHeaderStyle === MinimapSectionHeaderStyle.Underlined, + backgroundFill, + foregroundFill, + canvasInnerWidth, + backgroundFillY, + backgroundFillHeight, + y, + separatorY); + } + } + + private static _fitSectionHeader( + target: CanvasRenderingContext2D, + maxWidth: number, + headerText: string, + ): string { + if (!headerText) { + return headerText; + } + + const ellipsis = '…'; + const width = target.measureText(headerText).width; + const ellipsisWidth = target.measureText(ellipsis).width; + + if (width <= maxWidth || width <= ellipsisWidth) { + return headerText; + } + + const len = headerText.length; + const averageCharWidth = width / headerText.length; + const maxCharCount = Math.floor((maxWidth - ellipsisWidth) / averageCharWidth) - 1; + + // Find a halfway point that isn't after whitespace + let halfCharCount = Math.ceil(maxCharCount / 2); + while (halfCharCount > 0 && /\s/.test(headerText[halfCharCount - 1])) { + --halfCharCount; + } + + // Split with ellipsis + return headerText.substring(0, halfCharCount) + + ellipsis + headerText.substring(len - (maxCharCount - halfCharCount)); + } + + private static _renderSectionLabel( + target: CanvasRenderingContext2D, + headerText: string | null, + hasSeparatorLine: boolean, + backgroundFill: string, + foregroundFill: string, + minimapWidth: number, + backgroundFillY: number, + backgroundFillHeight: number, + textY: number, + separatorY: number + ): void { + if (headerText) { + target.fillStyle = backgroundFill; + target.fillRect(0, backgroundFillY, minimapWidth, backgroundFillHeight); + + target.fillStyle = foregroundFill; + target.fillText(headerText, MINIMAP_GUTTER_WIDTH, textY); + } + + if (hasSeparatorLine) { + target.beginPath(); + target.moveTo(0, separatorY); + target.lineTo(minimapWidth, separatorY); + target.closePath(); + target.stroke(); + } + } + private renderLines(layout: MinimapLayout): RenderData | null { const startLineNumber = layout.startLineNumber; const endLineNumber = layout.endLineNumber; diff --git a/code/src/vs/editor/browser/viewParts/selections/selections.ts b/code/src/vs/editor/browser/viewParts/selections/selections.ts index efceef0e5c3..d53a5126e62 100644 --- a/code/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/code/src/vs/editor/browser/viewParts/selections/selections.ts @@ -68,7 +68,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { private static readonly ROUNDED_PIECE_WIDTH = 10; private readonly _context: ViewContext; - private _lineHeight: number; private _roundedSelection: boolean; private _typicalHalfwidthCharacterWidth: number; private _selections: Range[]; @@ -78,7 +77,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { super(); this._context = context; const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; this._selections = []; @@ -96,7 +94,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); this._roundedSelection = options.get(EditorOption.roundedSelection); this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; return true; @@ -255,19 +252,16 @@ export class SelectionsOverlay extends DynamicViewOverlay { return linesVisibleRanges; } - private _createSelectionPiece(top: number, height: string, className: string, left: number, width: number): string { + private _createSelectionPiece(top: number, bottom: number, className: string, left: number, width: number): string { return ( '
' + + '" style="' + + 'top:' + top.toString() + 'px;' + + 'bottom:' + bottom.toString() + 'px;' + + 'left:' + left.toString() + 'px;' + + 'width:' + width.toString() + 'px;' + + '">
' ); } @@ -277,8 +271,6 @@ export class SelectionsOverlay extends DynamicViewOverlay { } const visibleRangesHaveStyle = !!visibleRanges[0].ranges[0].startStyle; - const fullLineHeight = (this._lineHeight).toString(); - const reducedLineHeight = (this._lineHeight - 1).toString(); const firstLineNumber = visibleRanges[0].lineNumber; const lastLineNumber = visibleRanges[visibleRanges.length - 1].lineNumber; @@ -288,8 +280,8 @@ export class SelectionsOverlay extends DynamicViewOverlay { const lineNumber = lineVisibleRanges.lineNumber; const lineIndex = lineNumber - visibleStartLineNumber; - const lineHeight = hasMultipleSelections ? (lineNumber === lastLineNumber || lineNumber === firstLineNumber ? reducedLineHeight : fullLineHeight) : fullLineHeight; const top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0; + const bottom = hasMultipleSelections ? (lineNumber !== firstLineNumber && lineNumber === lastLineNumber ? 1 : 0) : 0; let innerCornerOutput = ''; let restOfSelectionOutput = ''; @@ -304,7 +296,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { // Reverse rounded corner to the left // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -314,13 +306,13 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (startStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } - innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } if (endStyle.top === CornerStyle.INTERN || endStyle.bottom === CornerStyle.INTERN) { // Reverse rounded corner to the right // First comes the selection (blue layer) - innerCornerOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); // Second comes the background (white layer) with inverse border radius let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME; @@ -330,7 +322,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { if (endStyle.bottom === CornerStyle.INTERN) { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT; } - innerCornerOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); + innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH); } } @@ -351,7 +343,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT; } } - restOfSelectionOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left, visibleRange.width); + restOfSelectionOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left, visibleRange.width); } output2[lineIndex][0] += innerCornerOutput; diff --git a/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 489293a01b8..3bd29fc5e1e 100644 --- a/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -235,7 +235,7 @@ export class WhitespaceOverlay extends DynamicViewOverlay { if (USE_SVG) { maxLeft = Math.round(maxLeft + spaceWidth); return ( - `` + `` + result + `` ); diff --git a/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 39cba20b268..108aa52fa8a 100644 --- a/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -418,7 +418,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return null; } - return WordOperations.getWordAtPosition(this._modelData.model, this._configuration.options.get(EditorOption.wordSeparators), position); + return WordOperations.getWordAtPosition(this._modelData.model, this._configuration.options.get(EditorOption.wordSeparators), this._configuration.options.get(EditorOption.wordSegmenterLocales), position); } public getValue(options: { preserveBOM: boolean; lineEnding: string } | null = null): string { @@ -1042,8 +1042,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return; } case editorCommon.Handler.Paste: { - const args = >payload; - this._paste(source, args.text || '', args.pasteOnNewLine || false, args.multicursorText || null, args.mode || null); + const args = >payload; + this._paste(source, args.text || '', args.pasteOnNewLine || false, args.multicursorText || null, args.mode || null, args.clipboardEvent); return; } case editorCommon.Handler.Cut: @@ -1108,8 +1108,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._modelData.viewModel.compositionType(text, replacePrevCharCnt, replaceNextCharCnt, positionDelta, source); } - private _paste(source: string | null | undefined, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void { - if (!this._modelData || text.length === 0) { + private _paste(source: string | null | undefined, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null, clipboardEvent?: ClipboardEvent): void { + if (!this._modelData) { return; } const viewModel = this._modelData.viewModel; @@ -1118,6 +1118,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const endPosition = viewModel.getSelection().getStartPosition(); if (source === 'keyboard') { this._onDidPaste.fire({ + clipboardEvent, range: new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), languageId: mode }); @@ -1765,7 +1766,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } else { commandDelegate = { paste: (text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null) => { - const payload: editorCommon.PastePayload = { text, pasteOnNewLine, multicursorText, mode }; + const payload: editorBrowser.PastePayload = { text, pasteOnNewLine, multicursorText, mode }; this._commandService.executeCommand(editorCommon.Handler.Paste, payload); }, type: (text: string) => { diff --git a/code/src/vs/editor/browser/widget/codeEditor/editor.css b/code/src/vs/editor/browser/widget/codeEditor/editor.css index 1d60940158a..09c4a32f141 100644 --- a/code/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/code/src/vs/editor/browser/widget/codeEditor/editor.css @@ -56,6 +56,15 @@ top: 0; } +.monaco-editor .view-overlays > div, .monaco-editor .margin-view-overlays > div { + position: absolute; + width: 100%; +} + +.monaco-editor .view-overlays > div > div, .monaco-editor .margin-view-overlays > div > div { + bottom: 0; +} + /* .monaco-editor .auto-closed-character { opacity: 0.3; diff --git a/code/src/vs/editor/browser/widget/diffEditor/commands.ts b/code/src/vs/editor/browser/widget/diffEditor/commands.ts new file mode 100644 index 00000000000..0d29830b56e --- /dev/null +++ b/code/src/vs/editor/browser/widget/diffEditor/commands.ts @@ -0,0 +1,263 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveElement } from 'vs/base/browser/dom'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { localize2 } from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; +import { Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import './registrations.contribution'; +import { DiffEditorSelectionHunkToolbarContext } from 'vs/editor/browser/widget/diffEditor/features/gutterFeature'; + +export class ToggleCollapseUnchangedRegions extends Action2 { + constructor() { + super({ + id: 'diffEditor.toggleCollapseUnchangedRegions', + title: localize2('toggleCollapseUnchangedRegions', 'Toggle Collapse Unchanged Regions'), + icon: Codicon.map, + toggled: ContextKeyExpr.has('config.diffEditor.hideUnchangedRegions.enabled'), + precondition: ContextKeyExpr.has('isInDiffEditor'), + menu: { + when: ContextKeyExpr.has('isInDiffEditor'), + id: MenuId.EditorTitle, + order: 22, + group: 'navigation', + }, + }); + } + + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const configurationService = accessor.get(IConfigurationService); + const newValue = !configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); + configurationService.updateValue('diffEditor.hideUnchangedRegions.enabled', newValue); + } +} + +export class ToggleShowMovedCodeBlocks extends Action2 { + constructor() { + super({ + id: 'diffEditor.toggleShowMovedCodeBlocks', + title: localize2('toggleShowMovedCodeBlocks', 'Toggle Show Moved Code Blocks'), + precondition: ContextKeyExpr.has('isInDiffEditor'), + }); + } + + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const configurationService = accessor.get(IConfigurationService); + const newValue = !configurationService.getValue('diffEditor.experimental.showMoves'); + configurationService.updateValue('diffEditor.experimental.showMoves', newValue); + } +} + +export class ToggleUseInlineViewWhenSpaceIsLimited extends Action2 { + constructor() { + super({ + id: 'diffEditor.toggleUseInlineViewWhenSpaceIsLimited', + title: localize2('toggleUseInlineViewWhenSpaceIsLimited', 'Toggle Use Inline View When Space Is Limited'), + precondition: ContextKeyExpr.has('isInDiffEditor'), + }); + } + + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const configurationService = accessor.get(IConfigurationService); + const newValue = !configurationService.getValue('diffEditor.useInlineViewWhenSpaceIsLimited'); + configurationService.updateValue('diffEditor.useInlineViewWhenSpaceIsLimited', newValue); + } +} + +const diffEditorCategory: ILocalizedString = localize2('diffEditor', "Diff Editor"); + +export class SwitchSide extends EditorAction2 { + constructor() { + super({ + id: 'diffEditor.switchSide', + title: localize2('switchSide', 'Switch Side'), + icon: Codicon.arrowSwap, + precondition: ContextKeyExpr.has('isInDiffEditor'), + f1: true, + category: diffEditorCategory, + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, arg?: { dryRun: boolean }): unknown { + const diffEditor = findFocusedDiffEditor(accessor); + if (diffEditor instanceof DiffEditorWidget) { + if (arg && arg.dryRun) { + return { destinationSelection: diffEditor.mapToOtherSide().destinationSelection }; + } else { + diffEditor.switchSide(); + } + } + return undefined; + } +} +export class ExitCompareMove extends EditorAction2 { + constructor() { + super({ + id: 'diffEditor.exitCompareMove', + title: localize2('exitCompareMove', 'Exit Compare Move'), + icon: Codicon.close, + precondition: EditorContextKeys.comparingMovedCode, + f1: false, + category: diffEditorCategory, + keybinding: { + weight: 10000, + primary: KeyCode.Escape, + } + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): void { + const diffEditor = findFocusedDiffEditor(accessor); + if (diffEditor instanceof DiffEditorWidget) { + diffEditor.exitCompareMove(); + } + } +} + +export class CollapseAllUnchangedRegions extends EditorAction2 { + constructor() { + super({ + id: 'diffEditor.collapseAllUnchangedRegions', + title: localize2('collapseAllUnchangedRegions', 'Collapse All Unchanged Regions'), + icon: Codicon.fold, + precondition: ContextKeyExpr.has('isInDiffEditor'), + f1: true, + category: diffEditorCategory, + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): void { + const diffEditor = findFocusedDiffEditor(accessor); + if (diffEditor instanceof DiffEditorWidget) { + diffEditor.collapseAllUnchangedRegions(); + } + } +} + +export class ShowAllUnchangedRegions extends EditorAction2 { + constructor() { + super({ + id: 'diffEditor.showAllUnchangedRegions', + title: localize2('showAllUnchangedRegions', 'Show All Unchanged Regions'), + icon: Codicon.unfold, + precondition: ContextKeyExpr.has('isInDiffEditor'), + f1: true, + category: diffEditorCategory, + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): void { + const diffEditor = findFocusedDiffEditor(accessor); + if (diffEditor instanceof DiffEditorWidget) { + diffEditor.showAllUnchangedRegions(); + } + } +} + +export class RevertHunkOrSelection extends EditorAction2 { + constructor() { + super({ + id: 'diffEditor.revert', + title: localize2('revert', 'Revert'), + precondition: ContextKeyExpr.has('isInDiffEditor'), + f1: false, + category: diffEditorCategory, + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, arg: DiffEditorSelectionHunkToolbarContext): unknown { + const diffEditor = findFocusedDiffEditor(accessor); + if (diffEditor instanceof DiffEditorWidget) { + diffEditor.revertRangeMappings(arg.mapping.innerChanges ?? []); + } + return undefined; + } +} + +const accessibleDiffViewerCategory: ILocalizedString = localize2('accessibleDiffViewer', "Accessible Diff Viewer"); + +export class AccessibleDiffViewerNext extends Action2 { + public static id = 'editor.action.accessibleDiffViewer.next'; + + constructor() { + super({ + id: AccessibleDiffViewerNext.id, + title: localize2('editor.action.accessibleDiffViewer.next', 'Go to Next Difference'), + category: accessibleDiffViewerCategory, + precondition: ContextKeyExpr.has('isInDiffEditor'), + keybinding: { + primary: KeyCode.F7, + weight: KeybindingWeight.EditorContrib + }, + f1: true, + }); + } + + public override run(accessor: ServicesAccessor): void { + const diffEditor = findFocusedDiffEditor(accessor); + diffEditor?.accessibleDiffViewerNext(); + } +} + +export class AccessibleDiffViewerPrev extends Action2 { + public static id = 'editor.action.accessibleDiffViewer.prev'; + + constructor() { + super({ + id: AccessibleDiffViewerPrev.id, + title: localize2('editor.action.accessibleDiffViewer.prev', 'Go to Previous Difference'), + category: accessibleDiffViewerCategory, + precondition: ContextKeyExpr.has('isInDiffEditor'), + keybinding: { + primary: KeyMod.Shift | KeyCode.F7, + weight: KeybindingWeight.EditorContrib + }, + f1: true, + }); + } + + public override run(accessor: ServicesAccessor): void { + const diffEditor = findFocusedDiffEditor(accessor); + diffEditor?.accessibleDiffViewerPrev(); + } +} + +export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { + const codeEditorService = accessor.get(ICodeEditorService); + const diffEditors = codeEditorService.listDiffEditors(); + + const activeElement = getActiveElement(); + if (activeElement) { + for (const d of diffEditors) { + const container = d.getContainerDomNode(); + if (isElementOrParentOf(container, activeElement)) { + return d; + } + } + } + + return null; +} + +function isElementOrParentOf(elementOrParent: Element, element: Element): boolean { + let e: Element | null = element; + while (e) { + if (e === elementOrParent) { + return true; + } + e = e.parentElement; + } + return false; +} diff --git a/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index 1818ead661f..2abc8e74bad 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts @@ -3,64 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, autorunHandleChanges, derivedOpts, observableFromEvent } from 'vs/base/common/observable'; +import { IReader, autorunHandleChanges, derived, derivedOpts, observableFromEvent } from 'vs/base/common/observable'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { EditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; import { IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DiffEditorOptions } from '../diffEditorOptions'; -import { ITextModel } from 'vs/editor/common/model'; -import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; -import { Selection } from 'vs/editor/common/core/selection'; -import { Position } from 'vs/editor/common/core/position'; export class DiffEditorEditors extends Disposable { - public readonly modified: CodeEditorWidget; - public readonly original: CodeEditorWidget; + public readonly original = this._register(this._createLeftHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.originalEditor || {})); + public readonly modified = this._register(this._createRightHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.modifiedEditor || {})); private readonly _onDidContentSizeChange = this._register(new Emitter()); public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; } - public readonly modifiedScrollTop: IObservable; - public readonly modifiedScrollHeight: IObservable; + public readonly modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); + public readonly modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); + + public readonly modifiedModel = observableFromEvent(this.modified.onDidChangeModel, () => /** @description modified.model */ this.modified.getModel()); - public readonly modifiedModel: IObservable; + public readonly modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); + public readonly modifiedCursor = derivedOpts({ owner: this, equalityComparer: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); - public readonly modifiedSelections: IObservable; - public readonly modifiedCursor: IObservable; + public readonly originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); - public readonly originalCursor: IObservable; + public readonly isOriginalFocused = observableFromEvent(Event.any(this.original.onDidFocusEditorWidget, this.original.onDidBlurEditorWidget), () => this.original.hasWidgetFocus()); + public readonly isModifiedFocused = observableFromEvent(Event.any(this.modified.onDidFocusEditorWidget, this.modified.onDidBlurEditorWidget), () => this.modified.hasWidgetFocus()); + + public readonly isFocused = derived(this, reader => this.isOriginalFocused.read(reader) || this.isModifiedFocused.read(reader)); constructor( private readonly originalEditorElement: HTMLElement, private readonly modifiedEditorElement: HTMLElement, private readonly _options: DiffEditorOptions, - codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, + private _argCodeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, private readonly _createInnerEditor: (instantiationService: IInstantiationService, container: HTMLElement, options: Readonly, editorWidgetOptions: ICodeEditorWidgetOptions) => CodeEditorWidget, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); - this.original = this._register(this._createLeftHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.originalEditor || {})); - this.modified = this._register(this._createRightHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.modifiedEditor || {})); - - this.modifiedModel = observableFromEvent(this.modified.onDidChangeModel, () => /** @description modified.model */ this.modified.getModel()); - - this.modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); - this.modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); - - this.modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); - this.modifiedCursor = derivedOpts({ owner: this, equalityComparer: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); - - this.originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); + this._argCodeEditorWidgetOptions = null as any; this._register(autorunHandleChanges({ createEmptyChangeSummary: () => ({} as IDiffEditorConstructionOptions), diff --git a/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index d6bcac91567..23a75bac47d 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -312,7 +312,7 @@ export class DiffEditorViewZones extends Disposable { } let marginDomNode: HTMLElement | undefined = undefined; - if (a.diff && a.diff.modified.isEmpty && this._options.shouldRenderRevertArrows.read(reader)) { + if (a.diff && a.diff.modified.isEmpty && this._options.shouldRenderOldRevertArrows.read(reader)) { marginDomNode = createViewZoneMarginArrow(); } diff --git a/code/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts b/code/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts index 2cc7ffacdc4..75017bce4c3 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts @@ -3,83 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveElement } from 'vs/base/browser/dom'; import { Codicon } from 'vs/base/common/codicons'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev, CollapseAllUnchangedRegions, ExitCompareMove, RevertHunkOrSelection, ShowAllUnchangedRegions, SwitchSide, ToggleCollapseUnchangedRegions, ToggleShowMovedCodeBlocks, ToggleUseInlineViewWhenSpaceIsLimited } from 'vs/editor/browser/widget/diffEditor/commands'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { localize, localize2 } from 'vs/nls'; -import { ILocalizedString } from 'vs/platform/action/common/action'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import './registrations.contribution'; -export class ToggleCollapseUnchangedRegions extends Action2 { - constructor() { - super({ - id: 'diffEditor.toggleCollapseUnchangedRegions', - title: localize2('toggleCollapseUnchangedRegions', 'Toggle Collapse Unchanged Regions'), - icon: Codicon.map, - toggled: ContextKeyExpr.has('config.diffEditor.hideUnchangedRegions.enabled'), - precondition: ContextKeyExpr.has('isInDiffEditor'), - menu: { - when: ContextKeyExpr.has('isInDiffEditor'), - id: MenuId.EditorTitle, - order: 22, - group: 'navigation', - }, - }); - } - - run(accessor: ServicesAccessor, ...args: unknown[]): void { - const configurationService = accessor.get(IConfigurationService); - const newValue = !configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); - configurationService.updateValue('diffEditor.hideUnchangedRegions.enabled', newValue); - } -} - registerAction2(ToggleCollapseUnchangedRegions); - -export class ToggleShowMovedCodeBlocks extends Action2 { - constructor() { - super({ - id: 'diffEditor.toggleShowMovedCodeBlocks', - title: localize2('toggleShowMovedCodeBlocks', 'Toggle Show Moved Code Blocks'), - precondition: ContextKeyExpr.has('isInDiffEditor'), - }); - } - - run(accessor: ServicesAccessor, ...args: unknown[]): void { - const configurationService = accessor.get(IConfigurationService); - const newValue = !configurationService.getValue('diffEditor.experimental.showMoves'); - configurationService.updateValue('diffEditor.experimental.showMoves', newValue); - } -} - registerAction2(ToggleShowMovedCodeBlocks); - -export class ToggleUseInlineViewWhenSpaceIsLimited extends Action2 { - constructor() { - super({ - id: 'diffEditor.toggleUseInlineViewWhenSpaceIsLimited', - title: localize2('toggleUseInlineViewWhenSpaceIsLimited', 'Toggle Use Inline View When Space Is Limited'), - precondition: ContextKeyExpr.has('isInDiffEditor'), - }); - } - - run(accessor: ServicesAccessor, ...args: unknown[]): void { - const configurationService = accessor.get(IConfigurationService); - const newValue = !configurationService.getValue('diffEditor.useInlineViewWhenSpaceIsLimited'); - configurationService.updateValue('diffEditor.useInlineViewWhenSpaceIsLimited', newValue); - } -} - registerAction2(ToggleUseInlineViewWhenSpaceIsLimited); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { @@ -110,130 +44,41 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { when: ContextKeyExpr.has('isInDiffEditor'), }); -const diffEditorCategory: ILocalizedString = localize2('diffEditor', "Diff Editor"); - -export class SwitchSide extends EditorAction2 { - constructor() { - super({ - id: 'diffEditor.switchSide', - title: localize2('switchSide', 'Switch Side'), - icon: Codicon.arrowSwap, - precondition: ContextKeyExpr.has('isInDiffEditor'), - f1: true, - category: diffEditorCategory, - }); - } +registerAction2(RevertHunkOrSelection); + +for (const ctx of [ + { icon: Codicon.arrowRight, key: EditorContextKeys.diffEditorInlineMode.toNegated() }, + { icon: Codicon.discard, key: EditorContextKeys.diffEditorInlineMode } +]) { + MenuRegistry.appendMenuItem(MenuId.DiffEditorHunkToolbar, { + command: { + id: new RevertHunkOrSelection().desc.id, + title: localize('revertHunk', "Revert Block"), + icon: ctx.icon, + }, + when: ContextKeyExpr.and(EditorContextKeys.diffEditorModifiedWritable, ctx.key), + order: 5, + group: 'primary', + }); + + MenuRegistry.appendMenuItem(MenuId.DiffEditorSelectionToolbar, { + command: { + id: new RevertHunkOrSelection().desc.id, + title: localize('revertSelection', "Revert Selection"), + icon: ctx.icon, + }, + when: ContextKeyExpr.and(EditorContextKeys.diffEditorModifiedWritable, ctx.key), + order: 5, + group: 'primary', + }); - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, arg?: { dryRun: boolean }): unknown { - const diffEditor = findFocusedDiffEditor(accessor); - if (diffEditor instanceof DiffEditorWidget) { - if (arg && arg.dryRun) { - return { destinationSelection: diffEditor.mapToOtherSide().destinationSelection }; - } else { - diffEditor.switchSide(); - } - } - return undefined; - } } registerAction2(SwitchSide); - -export class ExitCompareMove extends EditorAction2 { - constructor() { - super({ - id: 'diffEditor.exitCompareMove', - title: localize2('exitCompareMove', 'Exit Compare Move'), - icon: Codicon.close, - precondition: EditorContextKeys.comparingMovedCode, - f1: false, - category: diffEditorCategory, - keybinding: { - weight: 10000, - primary: KeyCode.Escape, - } - }); - } - - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): void { - const diffEditor = findFocusedDiffEditor(accessor); - if (diffEditor instanceof DiffEditorWidget) { - diffEditor.exitCompareMove(); - } - } -} - registerAction2(ExitCompareMove); - -export class CollapseAllUnchangedRegions extends EditorAction2 { - constructor() { - super({ - id: 'diffEditor.collapseAllUnchangedRegions', - title: localize2('collapseAllUnchangedRegions', 'Collapse All Unchanged Regions'), - icon: Codicon.fold, - precondition: ContextKeyExpr.has('isInDiffEditor'), - f1: true, - category: diffEditorCategory, - }); - } - - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): void { - const diffEditor = findFocusedDiffEditor(accessor); - if (diffEditor instanceof DiffEditorWidget) { - diffEditor.collapseAllUnchangedRegions(); - } - } -} - registerAction2(CollapseAllUnchangedRegions); - -export class ShowAllUnchangedRegions extends EditorAction2 { - constructor() { - super({ - id: 'diffEditor.showAllUnchangedRegions', - title: localize2('showAllUnchangedRegions', 'Show All Unchanged Regions'), - icon: Codicon.unfold, - precondition: ContextKeyExpr.has('isInDiffEditor'), - f1: true, - category: diffEditorCategory, - }); - } - - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): void { - const diffEditor = findFocusedDiffEditor(accessor); - if (diffEditor instanceof DiffEditorWidget) { - diffEditor.showAllUnchangedRegions(); - } - } -} - registerAction2(ShowAllUnchangedRegions); -const accessibleDiffViewerCategory: ILocalizedString = localize2('accessibleDiffViewer', "Accessible Diff Viewer"); - -export class AccessibleDiffViewerNext extends Action2 { - public static id = 'editor.action.accessibleDiffViewer.next'; - - constructor() { - super({ - id: AccessibleDiffViewerNext.id, - title: localize2('editor.action.accessibleDiffViewer.next', 'Go to Next Difference'), - category: accessibleDiffViewerCategory, - precondition: ContextKeyExpr.has('isInDiffEditor'), - keybinding: { - primary: KeyCode.F7, - weight: KeybindingWeight.EditorContrib - }, - f1: true, - }); - } - - public override run(accessor: ServicesAccessor): void { - const diffEditor = findFocusedDiffEditor(accessor); - diffEditor?.accessibleDiffViewerNext(); - } -} - MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: AccessibleDiffViewerNext.id, @@ -248,56 +93,6 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { ), }); -export class AccessibleDiffViewerPrev extends Action2 { - public static id = 'editor.action.accessibleDiffViewer.prev'; - - constructor() { - super({ - id: AccessibleDiffViewerPrev.id, - title: localize2('editor.action.accessibleDiffViewer.prev', 'Go to Previous Difference'), - category: accessibleDiffViewerCategory, - precondition: ContextKeyExpr.has('isInDiffEditor'), - keybinding: { - primary: KeyMod.Shift | KeyCode.F7, - weight: KeybindingWeight.EditorContrib - }, - f1: true, - }); - } - - public override run(accessor: ServicesAccessor): void { - const diffEditor = findFocusedDiffEditor(accessor); - diffEditor?.accessibleDiffViewerPrev(); - } -} - -export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { - const codeEditorService = accessor.get(ICodeEditorService); - const diffEditors = codeEditorService.listDiffEditors(); - - const activeElement = getActiveElement(); - if (activeElement) { - for (const d of diffEditors) { - const container = d.getContainerDomNode(); - if (isElementOrParentOf(container, activeElement)) { - return d; - } - } - } - - return null; -} - -function isElementOrParentOf(elementOrParent: Element, element: Element): boolean { - let e: Element | null = element; - while (e) { - if (e === elementOrParent) { - return true; - } - e = e.parentElement; - } - return false; -} CommandsRegistry.registerCommandAlias('editor.action.diffReview.next', AccessibleDiffViewerNext.id); registerAction2(AccessibleDiffViewerNext); diff --git a/code/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/code/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index 1db9c84ce24..4056521fc97 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -32,12 +32,15 @@ export class DiffEditorOptions { ); public readonly readOnly = derived(this, reader => this._options.read(reader).readOnly); - public readonly shouldRenderRevertArrows = derived(this, reader => { + public readonly shouldRenderOldRevertArrows = derived(this, reader => { if (!this._options.read(reader).renderMarginRevertIcon) { return false; } if (!this.renderSideBySide.read(reader)) { return false; } if (this.readOnly.read(reader)) { return false; } + if (this.shouldRenderGutterMenu.read(reader)) { return false; } return true; }); + + public readonly shouldRenderGutterMenu = derived(this, reader => this._options.read(reader).renderGutterMenu); public readonly renderIndicators = derived(this, reader => this._options.read(reader).renderIndicators); public readonly enableSplitViewResizing = derived(this, reader => this._options.read(reader).enableSplitViewResizing); public readonly splitViewDefaultRatio = derived(this, reader => this._options.read(reader).splitViewDefaultRatio); @@ -99,5 +102,6 @@ function validateDiffEditorOptions(options: Readonly, defaul onlyShowAccessibleDiffViewer: validateBooleanOption(options.onlyShowAccessibleDiffViewer, defaults.onlyShowAccessibleDiffViewer), renderSideBySideInlineBreakpoint: clampedInt(options.renderSideBySideInlineBreakpoint, defaults.renderSideBySideInlineBreakpoint, 0, Constants.MAX_SAFE_SMALL_INTEGER), useInlineViewWhenSpaceIsLimited: validateBooleanOption(options.useInlineViewWhenSpaceIsLimited, defaults.useInlineViewWhenSpaceIsLimited), + renderGutterMenu: validateBooleanOption(options.renderGutterMenu, defaults.renderGutterMenu), }; } diff --git a/code/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/code/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 7499fa8e0bc..e69946f7ea3 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -30,7 +30,7 @@ import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; +import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; @@ -45,6 +45,7 @@ import { DiffEditorEditors } from './components/diffEditorEditors'; import { DelegatingEditor } from './delegatingEditorImpl'; import { DiffEditorOptions } from './diffEditorOptions'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel'; +import { DiffEditorGutter } from 'vs/editor/browser/widget/diffEditor/features/gutterFeature'; export interface IDiffCodeEditorWidgetOptions { originalEditor?: ICodeEditorWidgetOptions; @@ -56,8 +57,8 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [ h('div.noModificationsOverlay@overlay', { style: { position: 'absolute', height: '100%', visibility: 'hidden', } }, [$('span', {}, 'No Changes')]), - h('div.editor.original@original', { style: { position: 'absolute', height: '100%' } }), - h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%' } }), + h('div.editor.original@original', { style: { position: 'absolute', height: '100%', zIndex: '1', } }), + h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%', zIndex: '1', } }), h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }), ]); private readonly _diffModel = observableValue(this, undefined); @@ -72,6 +73,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { ); private readonly _rootSizeObserver: ObservableElementSizeObserver; + /** + * Is undefined if and only if side-by-side + */ private readonly _sash: IObservable; private readonly _boundarySashes = observableValue(this, undefined); @@ -88,6 +92,8 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { private readonly _overviewRulerPart: IObservable; private readonly _movedBlocksLinesPart = observableValue(this, undefined); + private readonly _gutter: IObservable; + public get collapseUnchangedRegions() { return this._options.hideUnchangedRegions.get(); } constructor( @@ -126,6 +132,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._register(bindContextKey(EditorContextKeys.diffEditorRenderSideBySideInlineBreakpointReached, this._contextKeyService, reader => this._options.couldShowInlineViewBecauseOfSize.read(reader) )); + this._register(bindContextKey(EditorContextKeys.diffEditorInlineMode, this._contextKeyService, + reader => !this._options.renderSideBySide.read(reader) + )); this._register(bindContextKey(EditorContextKeys.hasChanges, this._contextKeyService, reader => (this._diffModel.read(reader)?.diff.read(reader)?.mappings.length ?? 0) > 0 @@ -140,6 +149,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { (i, c, o, o2) => this._createInnerEditor(i, c, o, o2), )); + this._register(bindContextKey(EditorContextKeys.diffEditorOriginalWritable, this._contextKeyService, + reader => this._options.originalEditable.read(reader) + )); + this._register(bindContextKey(EditorContextKeys.diffEditorModifiedWritable, this._contextKeyService, + reader => !this._options.readOnly.read(reader) + )); + this._register(bindContextKey(EditorContextKeys.diffEditorOriginalUri, this._contextKeyService, + reader => this._diffModel.read(reader)?.model.original.uri.toString() ?? '' + )); + this._register(bindContextKey(EditorContextKeys.diffEditorModifiedUri, this._contextKeyService, + reader => this._diffModel.read(reader)?.model.modified.uri.toString() ?? '' + )); + this._overviewRulerPart = derivedDisposable(this, reader => !this._options.renderOverviewRuler.read(reader) ? undefined @@ -245,6 +267,17 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { codeEditorService.addDiffEditor(this); + this._gutter = derivedDisposable(this, reader => { + return this._options.shouldRenderGutterMenu.read(reader) + ? this._instantiationService.createInstance( + readHotReloadableExport(DiffEditorGutter, reader), + this.elements.root, + this._diffModel, + this._editors + ) + : undefined; + }); + this._register(recomputeInitiallyAndOnChange(this._layoutInfo)); derivedDisposable(this, reader => /** @description MovedBlocksLinesPart */ @@ -267,18 +300,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { ), })); - this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, (e) => { - if (e?.reason === CursorChangeReason.Explicit) { - const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber)); - if (diff?.lineRangeMapping.modified.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff?.lineRangeMapping.original.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); - } - } - })); + this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, true))); + this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, false))); + const isInitializingDiff = this._diffModel.map(this, (m, reader) => { /** @isInitializingDiff isDiffUpToDate */ @@ -299,7 +323,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } })); - this._register(new RevertButtonsFeature(this._editors, this._diffModel, this._options, this)); + this._register(autorunWithStore((reader, store) => { + store.add(new (readHotReloadableExport(RevertButtonsFeature, reader))(this._editors, this._diffModel, this._options, this)); + })); } public getViewWidth(): number { @@ -316,23 +342,49 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } private readonly _layoutInfo = derived(this, reader => { - const width = this._rootSizeObserver.width.read(reader); - const height = this._rootSizeObserver.height.read(reader); - const sashLeft = this._sash.read(reader)?.sashLeft.read(reader); + const fullWidth = this._rootSizeObserver.width.read(reader); + const fullHeight = this._rootSizeObserver.height.read(reader); - const originalWidth = sashLeft ?? Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft); - const modifiedWidth = width - originalWidth - (this._overviewRulerPart.read(reader)?.width ?? 0); + const sash = this._sash.read(reader); - 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'; + const gutter = this._gutter.read(reader); + const gutterWidth = gutter?.width.read(reader) ?? 0; - this.elements.modified.style.width = modifiedWidth + 'px'; - this.elements.modified.style.left = originalWidth + 'px'; + const overviewRulerPartWidth = this._overviewRulerPart.read(reader)?.width ?? 0; + + let originalLeft: number, originalWidth: number, modifiedLeft: number, modifiedWidth: number, gutterLeft: number; + + const sideBySide = !!sash; + if (sideBySide) { + const sashLeft = sash.sashLeft.read(reader); + const movedBlocksLinesWidth = this._movedBlocksLinesPart.read(reader)?.width.read(reader) ?? 0; + + originalLeft = 0; + originalWidth = sashLeft - gutterWidth - movedBlocksLinesWidth; + + gutterLeft = sashLeft - gutterWidth; + + modifiedLeft = sashLeft; + modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; + } else { + gutterLeft = 0; + + originalLeft = gutterWidth; + originalWidth = Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft); - this._editors.original.layout({ width: originalWidthWithoutMovedBlockLines, height }, true); - this._editors.modified.layout({ width: modifiedWidth, height }, true); + modifiedLeft = gutterWidth + originalWidth; + modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; + } + + this.elements.original.style.left = originalLeft + 'px'; + this.elements.original.style.width = originalWidth + 'px'; + this._editors.original.layout({ width: originalWidth, height: fullHeight }, true); + + gutter?.layout(gutterLeft); + + this.elements.modified.style.left = modifiedLeft + 'px'; + this.elements.modified.style.width = modifiedWidth + 'px'; + this._editors.modified.layout({ width: modifiedWidth, height: fullHeight }, true); return { modifiedEditor: this._editors.modified.getLayoutInfo(), @@ -608,6 +660,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } }); } + + private _handleCursorPositionChange(e: ICursorPositionChangedEvent | undefined, isModifiedEditor: boolean): void { + if (e?.reason === CursorChangeReason.Explicit) { + const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => isModifiedEditor ? m.lineRangeMapping.modified.contains(e.position.lineNumber) : m.lineRangeMapping.original.contains(e.position.lineNumber)); + if (diff?.lineRangeMapping.modified.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff?.lineRangeMapping.original.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); + } + } + } } function toLineChanges(state: DiffState): ILineChange[] { diff --git a/code/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/code/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts new file mode 100644 index 00000000000..900184578b5 --- /dev/null +++ b/code/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -0,0 +1,276 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EventType, addDisposableListener, h } from 'vs/base/browser/dom'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IObservable, autorun, autorunWithStore, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; +import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; +import { appendRemoveOnDispose, applyStyle } from 'vs/editor/browser/widget/diffEditor/utils'; +import { EditorGutter, IGutterItemInfo, IGutterItemView } from 'vs/editor/browser/widget/diffEditor/utils/editorGutter'; +import { ActionRunnerWithContext } from 'vs/editor/browser/widget/multiDiffEditor/utils'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { Range } from 'vs/editor/common/core/range'; +import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { TextModelText } from 'vs/editor/common/model/textModelText'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +const emptyArr: never[] = []; +const width = 35; + +export class DiffEditorGutter extends Disposable { + private readonly _menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService)); + private readonly _actions = observableFromEvent(this._menu.onDidChange, () => this._menu.getActions()); + private readonly _hasActions = this._actions.map(a => a.length > 0); + + public readonly width = derived(this, reader => this._hasActions.read(reader) ? width : 0); + + private readonly elements = h('div.gutter@gutter', { style: { position: 'absolute', height: '100%', width: width + 'px', zIndex: '0' } }, []); + + constructor( + diffEditorRoot: HTMLDivElement, + private readonly _diffModel: IObservable, + private readonly _editors: DiffEditorEditors, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IMenuService private readonly _menuService: IMenuService, + ) { + super(); + + this._register(appendRemoveOnDispose(diffEditorRoot, this.elements.root)); + + this._register(addDisposableListener(this.elements.root, 'click', () => { + this._editors.modified.focus(); + })); + + this._register(applyStyle(this.elements.root, { display: this._hasActions.map(a => a ? 'block' : 'none') })); + + this._register(new EditorGutter(this._editors.modified, this.elements.root, { + getIntersectingGutterItems: (range, reader) => { + const model = this._diffModel.read(reader); + if (!model) { + return []; + } + const diffs = model.diff.read(reader); + if (!diffs) { return []; } + + const selection = this._selectedDiffs.read(reader); + if (selection.length > 0) { + const m = DetailedLineRangeMapping.fromRangeMappings(selection.flatMap(s => s.rangeMappings)); + return [new DiffGutterItem(m, true, MenuId.DiffEditorSelectionToolbar, undefined)]; + } + + const currentDiff = this._currentDiff.read(reader); + + return diffs.mappings.map(m => new DiffGutterItem( + m.lineRangeMapping, + m.lineRangeMapping === currentDiff?.lineRangeMapping, + MenuId.DiffEditorHunkToolbar, + undefined, + )); + }, + createView: (item, target) => { + return this._instantiationService.createInstance(DiffToolBar, item, target, this); + }, + })); + + this._register(addDisposableListener(this.elements.gutter, EventType.MOUSE_WHEEL, (e: IMouseWheelEvent) => { + if (!this._editors.modified.getOption(EditorOption.scrollbar).handleMouseWheel) { + this._editors.modified.delegateScrollFromMouseWheelEvent(e); + } + }, { passive: false })); + } + + public computeStagedValue(mapping: DetailedLineRangeMapping): string { + const c = mapping.innerChanges ?? []; + const edit = new TextEdit(c.map(c => new SingleTextEdit(c.originalRange, this._editors.modifiedModel.get()!.getValueInRange(c.modifiedRange)))); + const value = edit.apply(new TextModelText(this._editors.original.getModel()!)); + return value; + } + + private readonly _currentDiff = derived(this, (reader) => { + const model = this._diffModel.read(reader); + if (!model) { + return undefined; + } + const mappings = model.diff.read(reader)?.mappings; + + const cursorPosition = this._editors.modifiedCursor.read(reader); + if (!cursorPosition) { return undefined; } + + return mappings?.find(m => m.lineRangeMapping.modified.contains(cursorPosition.lineNumber)); + }); + + private readonly _selectedDiffs = derived(this, (reader) => { + /** @description selectedDiffs */ + const model = this._diffModel.read(reader); + const diff = model?.diff.read(reader); + // Return `emptyArr` because it is a constant. [] is always a new array and would trigger a change. + if (!diff) { return emptyArr; } + + const selections = this._editors.modifiedSelections.read(reader); + if (selections.every(s => s.isEmpty())) { return emptyArr; } + + const selectedLineNumbers = new LineRangeSet(selections.map(s => LineRange.fromRangeInclusive(s))); + + const selectedMappings = diff.mappings.filter(m => + m.lineRangeMapping.innerChanges && selectedLineNumbers.intersects(m.lineRangeMapping.modified) + ); + const result = selectedMappings.map(mapping => ({ + mapping, + rangeMappings: mapping.lineRangeMapping.innerChanges!.filter( + c => selections.some(s => Range.areIntersecting(c.modifiedRange, s)) + ) + })); + if (result.length === 0 || result.every(r => r.rangeMappings.length === 0)) { return emptyArr; } + return result; + }); + + layout(left: number) { + this.elements.gutter.style.left = left + 'px'; + } +} + +class DiffGutterItem implements IGutterItemInfo { + constructor( + public readonly mapping: DetailedLineRangeMapping, + public readonly showAlways: boolean, + public readonly menuId: MenuId, + public readonly rangeOverride: LineRange | undefined, + ) { + } + get id(): string { return this.mapping.modified.toString(); } + get range(): LineRange { return this.rangeOverride ?? this.mapping.modified; } +} + + +class DiffToolBar extends Disposable implements IGutterItemView { + private readonly _elements = h('div.gutterItem', { style: { height: '20px', width: '34px' } }, [ + h('div.background@background', {}, []), + h('div.buttons@buttons', {}, []), + ]); + + private readonly _showAlways = this._item.map(this, item => item.showAlways); + private readonly _menuId = this._item.map(this, item => item.menuId); + + private readonly _isSmall = observableValue(this, false); + + constructor( + private readonly _item: IObservable, + target: HTMLElement, + gutter: DiffEditorGutter, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(); + + //const r = new ObservableElementSizeObserver + + const hoverDelegate = this._register(instantiationService.createInstance( + WorkbenchHoverDelegate, + 'element', + true, + { position: { hoverPosition: HoverPosition.RIGHT } } + )); + + this._register(appendRemoveOnDispose(target, this._elements.root)); + + this._register(autorun(reader => { + /** @description update showAlways */ + const showAlways = this._showAlways.read(reader); + this._elements.root.classList.toggle('noTransition', true); + this._elements.root.classList.toggle('showAlways', showAlways); + setTimeout(() => { + this._elements.root.classList.toggle('noTransition', false); + }, 0); + })); + + + this._register(autorunWithStore((reader, store) => { + this._elements.buttons.replaceChildren(); + const i = store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.buttons, this._menuId.read(reader), { + orientation: ActionsOrientation.VERTICAL, + hoverDelegate, + toolbarOptions: { + primaryGroup: g => g.startsWith('primary'), + }, + overflowBehavior: { maxItems: this._isSmall.read(reader) ? 1 : 3 }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + actionRunner: new ActionRunnerWithContext(() => { + const mapping = this._item.get().mapping; + return { + mapping, + originalWithModifiedChanges: gutter.computeStagedValue(mapping), + } satisfies DiffEditorSelectionHunkToolbarContext; + }), + menuOptions: { + shouldForwardArgs: true, + }, + })); + store.add(i.onDidChangeMenuItems(() => { + if (this._lastItemRange) { + this.layout(this._lastItemRange, this._lastViewRange!); + } + })); + })); + } + + private _lastItemRange: OffsetRange | undefined = undefined; + private _lastViewRange: OffsetRange | undefined = undefined; + + layout(itemRange: OffsetRange, viewRange: OffsetRange): void { + this._lastItemRange = itemRange; + this._lastViewRange = viewRange; + + let itemHeight = this._elements.buttons.clientHeight; + this._isSmall.set(this._item.get().mapping.original.startLineNumber === 1 && itemRange.length < 30, undefined); + // Item might have changed + itemHeight = this._elements.buttons.clientHeight; + + this._elements.root.style.top = itemRange.start + 'px'; + this._elements.root.style.height = itemRange.length + 'px'; + + const middleHeight = itemRange.length / 2 - itemHeight / 2; + + const margin = itemHeight; + + let effectiveCheckboxTop = itemRange.start + middleHeight; + + const preferredViewPortRange = OffsetRange.tryCreate( + margin, + viewRange.endExclusive - margin - itemHeight + ); + + const preferredParentRange = OffsetRange.tryCreate( + itemRange.start + margin, + itemRange.endExclusive - itemHeight - margin + ); + + if (preferredParentRange && preferredViewPortRange && preferredParentRange.start < preferredParentRange.endExclusive) { + effectiveCheckboxTop = preferredViewPortRange!.clip(effectiveCheckboxTop); + effectiveCheckboxTop = preferredParentRange!.clip(effectiveCheckboxTop); + } + + this._elements.buttons.style.top = `${effectiveCheckboxTop - itemRange.start}px`; + } +} + +export interface DiffEditorSelectionHunkToolbarContext { + mapping: DetailedLineRangeMapping; + + /** + * The original text with the selected modified changes applied. + */ + originalWithModifiedChanges: string; +} diff --git a/code/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts b/code/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts index 532786f2da1..b2a7d382320 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts @@ -31,7 +31,7 @@ export class RevertButtonsFeature extends Disposable { super(); this._register(autorunWithStore((reader, store) => { - if (!this._options.shouldRenderRevertArrows.read(reader)) { return; } + if (!this._options.shouldRenderOldRevertArrows.read(reader)) { return; } const model = this._diffModel.read(reader); const diff = model?.diff.read(reader); if (!model || !diff) { return; } diff --git a/code/src/vs/editor/browser/widget/diffEditor/style.css b/code/src/vs/editor/browser/widget/diffEditor/style.css index 032ff0f19d7..49ad115e36b 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/style.css +++ b/code/src/vs/editor/browser/widget/diffEditor/style.css @@ -294,6 +294,11 @@ border-left: 1px solid var(--vscode-diffEditor-border); } +.monaco-diff-editor.side-by-side .editor.original { + box-shadow: 6px 0 5px -5px var(--vscode-scrollbar-shadow); + border-right: 1px solid var(--vscode-diffEditor-border); +} + .monaco-diff-editor .diffViewport { background: var(--vscode-scrollbarSlider-background); } @@ -316,3 +321,74 @@ ); background-size: 8px 8px; } + +.monaco-diff-editor .gutter { + position: relative; + overflow: hidden; + flex-shrink: 0; + flex-grow: 0; + + .gutterItem { + opacity: 0; + transition: opacity 0.7s; + + &.showAlways { + opacity: 1; + transition: none; + } + + &.noTransition { + transition: none; + } + } + + &:hover .gutterItem { + opacity: 1; + transition: opacity 0.1s ease-in-out; + } + + .gutterItem { + .background { + position: absolute; + height: 100%; + left: 50%; + width: 1px; + + border-left: 2px var(--vscode-menu-border) solid; + } + + .buttons { + position: absolute; + /*height: 100%;*/ + width: 100%; + + display: flex; + justify-content: center; + align-items: center; + + .monaco-toolbar { + height: fit-content; + .monaco-action-bar { + line-height: 1; + + .actions-container { + width: fit-content; + border-radius: 4px; + border: 1px var(--vscode-menu-border) solid; + background: var(--vscode-editor-background); + + .action-item { + &:hover { + background: var(--vscode-toolbar-hoverBackground); + } + + .action-label { + padding: 0.5px 1px; + } + } + } + } + } + } + } +} diff --git a/code/src/vs/editor/browser/widget/diffEditor/utils.ts b/code/src/vs/editor/browser/widget/diffEditor/utils.ts index b71da8bb55d..a1e263948f2 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -421,23 +421,15 @@ export function translatePosition(posInOriginal: Position, mappings: DetailedLin return innerMapping.modifiedRange; } else { const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal); - return Range.fromPositions(addLength(innerMapping.modifiedRange.getEndPosition(), l)); + return Range.fromPositions(l.addToPosition(innerMapping.modifiedRange.getEndPosition())); } } -function lengthBetweenPositions(position1: Position, position2: Position): LengthObj { +function lengthBetweenPositions(position1: Position, position2: Position): TextLength { if (position1.lineNumber === position2.lineNumber) { - return new LengthObj(0, position2.column - position1.column); + return new TextLength(0, position2.column - position1.column); } else { - return new LengthObj(position2.lineNumber - position1.lineNumber, position2.column - 1); - } -} - -function addLength(position: Position, length: LengthObj): Position { - if (length.lineCount === 0) { - return new Position(position.lineNumber, position.column + length.columnCount); - } else { - return new Position(position.lineNumber + length.lineCount, length.columnCount + 1); + return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1); } } diff --git a/code/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts b/code/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts new file mode 100644 index 00000000000..8459f2a1c66 --- /dev/null +++ b/code/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h, reset } from 'vs/base/browser/dom'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { autorun, IObservable, IReader, ISettableObservable, observableFromEvent, observableSignal, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; + +export class EditorGutter extends Disposable { + private readonly scrollTop = observableFromEvent( + this._editor.onDidScrollChange, + (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() + ); + private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); + private readonly modelAttached = observableFromEvent( + this._editor.onDidChangeModel, + (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() + ); + + private readonly editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones); + private readonly editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange); + private readonly domNodeSizeChanged = observableSignal('domNodeSizeChanged'); + + constructor( + private readonly _editor: CodeEditorWidget, + private readonly _domNode: HTMLElement, + private readonly itemProvider: IGutterItemProvider + ) { + super(); + this._domNode.className = 'gutter monaco-editor'; + const scrollDecoration = this._domNode.appendChild( + h('div.scroll-decoration', { role: 'presentation', ariaHidden: 'true', style: { width: '100%' } }) + .root + ); + + const o = new ResizeObserver(() => { + transaction(tx => { + /** @description ResizeObserver: size changed */ + this.domNodeSizeChanged.trigger(tx); + }); + }); + o.observe(this._domNode); + this._register(toDisposable(() => o.disconnect())); + + this._register(autorun(reader => { + /** @description update scroll decoration */ + scrollDecoration.className = this.isScrollTopZero.read(reader) ? '' : 'scroll-decoration'; + })); + + this._register(autorun(reader => /** @description EditorGutter.Render */ this.render(reader))); + } + + override dispose(): void { + super.dispose(); + + reset(this._domNode); + } + + private readonly views = new Map(); + + private render(reader: IReader): void { + if (!this.modelAttached.read(reader)) { + return; + } + + this.domNodeSizeChanged.read(reader); + this.editorOnDidChangeViewZones.read(reader); + this.editorOnDidContentSizeChange.read(reader); + + const scrollTop = this.scrollTop.read(reader); + + const visibleRanges = this._editor.getVisibleRanges(); + const unusedIds = new Set(this.views.keys()); + + const viewRange = OffsetRange.ofStartAndLength(0, this._domNode.clientHeight); + + if (!viewRange.isEmpty) { + for (const visibleRange of visibleRanges) { + const visibleRange2 = new LineRange( + visibleRange.startLineNumber, + visibleRange.endLineNumber + 1 + ); + + const gutterItems = this.itemProvider.getIntersectingGutterItems( + visibleRange2, + reader + ); + + transaction(tx => { + /** EditorGutter.render */ + + for (const gutterItem of gutterItems) { + if (!gutterItem.range.intersect(visibleRange2)) { + continue; + } + + unusedIds.delete(gutterItem.id); + let view = this.views.get(gutterItem.id); + if (!view) { + const viewDomNode = document.createElement('div'); + this._domNode.appendChild(viewDomNode); + const gutterItemObs = observableValue('item', gutterItem); + const itemView = this.itemProvider.createView( + gutterItemObs, + viewDomNode + ); + view = new ManagedGutterItemView(gutterItemObs, itemView, viewDomNode); + this.views.set(gutterItem.id, view); + } else { + view.item.set(gutterItem, tx); + } + + const top = + gutterItem.range.startLineNumber <= this._editor.getModel()!.getLineCount() + ? this._editor.getTopForLineNumber(gutterItem.range.startLineNumber, true) - scrollTop + : this._editor.getBottomForLineNumber(gutterItem.range.startLineNumber - 1, false) - scrollTop; + const bottom = this._editor.getBottomForLineNumber(gutterItem.range.endLineNumberExclusive - 1, true) - scrollTop; + + const height = bottom - top; + + view.domNode.style.top = `${top}px`; + view.domNode.style.height = `${height}px`; + + view.gutterItemView.layout(OffsetRange.ofStartAndLength(top, height), viewRange); + } + }); + } + } + + for (const id of unusedIds) { + const view = this.views.get(id)!; + view.gutterItemView.dispose(); + this._domNode.removeChild(view.domNode); + this.views.delete(id); + } + } +} + +class ManagedGutterItemView { + constructor( + public readonly item: ISettableObservable, + public readonly gutterItemView: IGutterItemView, + public readonly domNode: HTMLDivElement, + ) { } +} + +export interface IGutterItemProvider { + getIntersectingGutterItems(range: LineRange, reader: IReader): TItem[]; + + createView(item: IObservable, target: HTMLElement): IGutterItemView; +} + +export interface IGutterItemInfo { + id: string; + range: LineRange; +} + +export interface IGutterItemView extends IDisposable { + layout(itemRange: OffsetRange, viewRange: OffsetRange): void; +} diff --git a/code/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/code/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index 84cffd7e94e..c6f3ca9de9e 100644 --- a/code/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/code/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -236,7 +236,7 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< }); } - private readonly _headerHeight = /*this._elements.header.clientHeight*/ 48; + private readonly _headerHeight = /*this._elements.header.clientHeight*/ 40; private _lastScrollTop = -1; private _isSettingScrollTop = false; @@ -285,6 +285,6 @@ function isFocused(editor: ICodeEditor): IObservable { store.add(editor.onDidBlurEditorWidget(() => h(false))); return store; }, - () => editor.hasWidgetFocus() + () => editor.hasTextFocus() ); } diff --git a/code/src/vs/editor/browser/widget/multiDiffEditor/style.css b/code/src/vs/editor/browser/widget/multiDiffEditor/style.css index 44f0703d580..c540a46b3f1 100644 --- a/code/src/vs/editor/browser/widget/multiDiffEditor/style.css +++ b/code/src/vs/editor/browser/widget/multiDiffEditor/style.css @@ -37,7 +37,7 @@ .header-content { margin: 8px 8px 0px 8px; - padding: 8px 5px; + padding: 4px 5px; border-top: 1px solid var(--vscode-multiDiffEditor-border); border-right: 1px solid var(--vscode-multiDiffEditor-border); diff --git a/code/src/vs/editor/browser/widget/multiDiffEditor/utils.ts b/code/src/vs/editor/browser/widget/multiDiffEditor/utils.ts index 43449e5827d..be9240267e1 100644 --- a/code/src/vs/editor/browser/widget/multiDiffEditor/utils.ts +++ b/code/src/vs/editor/browser/widget/multiDiffEditor/utils.ts @@ -6,11 +6,12 @@ import { ActionRunner, IAction } from 'vs/base/common/actions'; export class ActionRunnerWithContext extends ActionRunner { - constructor(private readonly _getContext: () => any) { + constructor(private readonly _getContext: () => unknown) { super(); } protected override runAction(action: IAction, _context?: unknown): Promise { - return super.runAction(action, this._getContext()); + const ctx = this._getContext(); + return super.runAction(action, ctx); } } diff --git a/code/src/vs/editor/common/commands/trimTrailingWhitespaceCommand.ts b/code/src/vs/editor/common/commands/trimTrailingWhitespaceCommand.ts index ebceaddc032..343a5739f19 100644 --- a/code/src/vs/editor/common/commands/trimTrailingWhitespaceCommand.ts +++ b/code/src/vs/editor/common/commands/trimTrailingWhitespaceCommand.ts @@ -9,6 +9,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ITextModel } from 'vs/editor/common/model'; export class TrimTrailingWhitespaceCommand implements ICommand { @@ -16,15 +17,17 @@ export class TrimTrailingWhitespaceCommand implements ICommand { private readonly _selection: Selection; private _selectionId: string | null; private readonly _cursors: Position[]; + private readonly _trimInRegexesAndStrings: boolean; - constructor(selection: Selection, cursors: Position[]) { + constructor(selection: Selection, cursors: Position[], trimInRegexesAndStrings: boolean) { this._selection = selection; this._cursors = cursors; this._selectionId = null; + this._trimInRegexesAndStrings = trimInRegexesAndStrings; } public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { - const ops = trimTrailingWhitespace(model, this._cursors); + const ops = trimTrailingWhitespace(model, this._cursors, this._trimInRegexesAndStrings); for (let i = 0, len = ops.length; i < len; i++) { const op = ops[i]; @@ -42,7 +45,7 @@ export class TrimTrailingWhitespaceCommand implements ICommand { /** * Generate commands for trimming trailing whitespace on a model and ignore lines on which cursors are sitting. */ -export function trimTrailingWhitespace(model: ITextModel, cursors: Position[]): ISingleEditOperation[] { +export function trimTrailingWhitespace(model: ITextModel, cursors: Position[], trimInRegexesAndStrings: boolean): ISingleEditOperation[] { // Sort cursors ascending cursors.sort((a, b) => { if (a.lineNumber === b.lineNumber) { @@ -96,6 +99,22 @@ export function trimTrailingWhitespace(model: ITextModel, cursors: Position[]): continue; } + if (!trimInRegexesAndStrings) { + if (!model.tokenization.hasAccurateTokensForLine(lineNumber)) { + // We don't want to force line tokenization, as that can be expensive, but we also don't want to trim + // trailing whitespace in lines that are not tokenized yet, as that can be wrong and trim whitespace from + // lines that the user requested we don't. So we bail out if the tokens are not accurate for this line. + continue; + } + + const lineTokens = model.tokenization.getLineTokens(lineNumber); + const fromColumnType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(fromColumn)); + + if (fromColumnType === StandardTokenType.String || fromColumnType === StandardTokenType.RegEx) { + continue; + } + } + fromColumn = Math.max(minEditColumn, fromColumn); r[rLen++] = EditOperation.delete(new Range( lineNumber, fromColumn, diff --git a/code/src/vs/editor/common/config/diffEditor.ts b/code/src/vs/editor/common/config/diffEditor.ts index 2a62c479848..2d0a312357e 100644 --- a/code/src/vs/editor/common/config/diffEditor.ts +++ b/code/src/vs/editor/common/config/diffEditor.ts @@ -10,6 +10,7 @@ export const diffEditorDefaultOptions = { splitViewDefaultRatio: 0.5, renderSideBySide: true, renderMarginRevertIcon: true, + renderGutterMenu: true, maxComputationTime: 5000, maxFileSize: 50, ignoreTrimWhitespace: true, diff --git a/code/src/vs/editor/common/config/editorConfigurationSchema.ts b/code/src/vs/editor/common/config/editorConfigurationSchema.ts index 3b22985f00d..ab2b8cc70c6 100644 --- a/code/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/code/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -175,6 +175,11 @@ const editorConfiguration: IConfigurationNode = { default: diffEditorDefaultOptions.renderMarginRevertIcon, description: nls.localize('renderMarginRevertIcon', "When enabled, the diff editor shows arrows in its glyph margin to revert changes.") }, + 'diffEditor.renderGutterMenu': { + type: 'boolean', + default: diffEditorDefaultOptions.renderGutterMenu, + description: nls.localize('renderGutterMenu', "When enabled, the diff editor shows a special gutter for revert and stage actions.") + }, 'diffEditor.ignoreTrimWhitespace': { type: 'boolean', default: diffEditorDefaultOptions.ignoreTrimWhitespace, diff --git a/code/src/vs/editor/common/config/editorOptions.ts b/code/src/vs/editor/common/config/editorOptions.ts index bd51c9ec27d..b2a486bce5c 100644 --- a/code/src/vs/editor/common/config/editorOptions.ts +++ b/code/src/vs/editor/common/config/editorOptions.ts @@ -75,6 +75,13 @@ export interface IEditorOptions { * Defaults to empty array. */ rulers?: (number | IRulerOption)[]; + /** + * Locales used for segmenting lines into words when doing word related navigations or operations. + * + * Specify the BCP 47 language tag of the word you wish to recognize (e.g., ja, zh-CN, zh-Hant-TW, etc.). + * Defaults to empty array + */ + wordSegmenterLocales?: string | string[]; /** * A string containing the word separators used when doing word navigation. * Defaults to `~!@#$%^&*()-=+[{]}\\|;:\'",.<>/? @@ -803,6 +810,10 @@ export interface IDiffEditorBaseOptions { * Default to true. */ renderMarginRevertIcon?: boolean; + /** + * Indicates if the gutter menu should be rendered. + */ + renderGutterMenu?: boolean; /** * Original model should be editable? * Defaults to false. @@ -3048,6 +3059,18 @@ export interface IEditorMinimapOptions { * Relative size of the font in the minimap. Defaults to 1. */ scale?: number; + /** + * Whether to show named regions as section headers. Defaults to true. + */ + showRegionSectionHeaders?: boolean; + /** + * Whether to show MARK: comments as section headers. Defaults to true. + */ + showMarkSectionHeaders?: boolean; + /** + * Font size of section headers. Defaults to 9. + */ + sectionHeaderFontSize?: number; } /** @@ -3067,6 +3090,9 @@ class EditorMinimap extends BaseEditorOption { + constructor() { + const defaults: string[] = []; + + super( + EditorOption.wordSegmenterLocales, 'wordSegmenterLocales', defaults, + { + anyOf: [ + { + description: nls.localize('wordSegmenterLocales', "Locales to be used for word segmentation when doing word related navigations or operations. Specify the BCP 47 language tag of the word you wish to recognize (e.g., ja, zh-CN, zh-Hant-TW, etc.)."), + type: 'string', + }, { + description: nls.localize('wordSegmenterLocales', "Locales to be used for word segmentation when doing word related navigations or operations. Specify the BCP 47 language tag of the word you wish to recognize (e.g., ja, zh-CN, zh-Hant-TW, etc.)."), + type: 'array', + items: { + type: 'string' + } + } + ] + } + ); + } + + public validate(input: any): string[] { + if (typeof input === 'string') { + input = [input]; + } + if (Array.isArray(input)) { + const validLocales: string[] = []; + for (const locale of input) { + if (typeof locale === 'string') { + try { + if (Intl.Segmenter.supportedLocalesOf(locale).length > 0) { + validLocales.push(locale); + } + } catch { + // ignore invalid locales + } + } + } + return validLocales; + } + + return this.defaultValue; + } +} + + //#endregion //#region wrappingIndent @@ -5284,6 +5385,7 @@ export const enum EditorOption { useShadowDOM, useTabStops, wordBreak, + wordSegmenterLocales, wordSeparators, wordWrap, wordWrapBreakAfterCharacters, @@ -5536,7 +5638,7 @@ export const EditorOptions = { nls.localize('cursorSurroundingLinesStyle.default', "`cursorSurroundingLines` is enforced only when triggered via the keyboard or API."), nls.localize('cursorSurroundingLinesStyle.all', "`cursorSurroundingLines` is enforced always.") ], - markdownDescription: nls.localize('cursorSurroundingLinesStyle', "Controls when `#cursorSurroundingLines#` should be enforced.") + markdownDescription: nls.localize('cursorSurroundingLinesStyle', "Controls when `#editor.cursorSurroundingLines#` should be enforced.") } )), cursorWidth: register(new EditorIntOption( @@ -5987,7 +6089,7 @@ export const EditorOptions = { )), useTabStops: register(new EditorBooleanOption( EditorOption.useTabStops, 'useTabStops', true, - { description: nls.localize('useTabStops', "Inserting and deleting whitespace follows tab stops.") } + { description: nls.localize('useTabStops', "Spaces and tabs are inserted and deleted in alignment with tab stops.") } )), wordBreak: register(new EditorStringEnumOption( EditorOption.wordBreak, 'wordBreak', @@ -6001,6 +6103,7 @@ export const EditorOptions = { description: nls.localize('wordBreak', "Controls the word break rules used for Chinese/Japanese/Korean (CJK) text.") } )), + wordSegmenterLocales: register(new WordSegmenterLocales()), wordSeparators: register(new EditorStringOption( EditorOption.wordSeparators, 'wordSeparators', USUAL_WORD_SEPARATORS, { description: nls.localize('wordSeparators', "Characters that will be used as word separators when doing word related navigations or operations.") } diff --git a/code/src/vs/editor/common/core/lineRange.ts b/code/src/vs/editor/common/core/lineRange.ts index da150f47952..1cbe63ceba1 100644 --- a/code/src/vs/editor/common/core/lineRange.ts +++ b/code/src/vs/editor/common/core/lineRange.ts @@ -52,6 +52,19 @@ export class LineRange { return result.ranges; } + public static join(lineRanges: LineRange[]): LineRange { + if (lineRanges.length === 0) { + throw new BugIndicatingError('lineRanges cannot be empty'); + } + let startLineNumber = lineRanges[0].startLineNumber; + let endLineNumberExclusive = lineRanges[0].endLineNumberExclusive; + for (let i = 1; i < lineRanges.length; i++) { + startLineNumber = Math.min(startLineNumber, lineRanges[i].startLineNumber); + endLineNumberExclusive = Math.max(endLineNumberExclusive, lineRanges[i].endLineNumberExclusive); + } + return new LineRange(startLineNumber, endLineNumberExclusive); + } + public static ofLength(startLineNumber: number, length: number): LineRange { return new LineRange(startLineNumber, startLineNumber + length); } diff --git a/code/src/vs/editor/common/core/positionToOffset.ts b/code/src/vs/editor/common/core/positionToOffset.ts new file mode 100644 index 00000000000..484c0a3265f --- /dev/null +++ b/code/src/vs/editor/common/core/positionToOffset.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { findLastIdxMonotonous } from 'vs/base/common/arraysFind'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { TextLength } from 'vs/editor/common/core/textLength'; + +export class PositionOffsetTransformer { + private readonly lineStartOffsetByLineIdx: number[]; + + constructor(public readonly text: string) { + this.lineStartOffsetByLineIdx = []; + this.lineStartOffsetByLineIdx.push(0); + for (let i = 0; i < text.length; i++) { + if (text.charAt(i) === '\n') { + this.lineStartOffsetByLineIdx.push(i + 1); + } + } + } + + getOffset(position: Position): number { + return this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1; + } + + getOffsetRange(range: Range): OffsetRange { + return new OffsetRange( + this.getOffset(range.getStartPosition()), + this.getOffset(range.getEndPosition()) + ); + } + + getPosition(offset: number): Position { + const idx = findLastIdxMonotonous(this.lineStartOffsetByLineIdx, i => i <= offset); + const lineNumber = idx + 1; + const column = offset - this.lineStartOffsetByLineIdx[idx] + 1; + return new Position(lineNumber, column); + } + + getRange(offsetRange: OffsetRange): Range { + return Range.fromPositions( + this.getPosition(offsetRange.start), + this.getPosition(offsetRange.endExclusive) + ); + } + + getTextLength(offsetRange: OffsetRange): TextLength { + return TextLength.ofRange(this.getRange(offsetRange)); + } + + get textLength(): TextLength { + const lineIdx = this.lineStartOffsetByLineIdx.length - 1; + return new TextLength(lineIdx, this.text.length - this.lineStartOffsetByLineIdx[lineIdx]); + } +} diff --git a/code/src/vs/editor/common/core/rangeMapping.ts b/code/src/vs/editor/common/core/rangeMapping.ts new file mode 100644 index 00000000000..379e046357d --- /dev/null +++ b/code/src/vs/editor/common/core/rangeMapping.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { findLastMonotonous } from 'vs/base/common/arraysFind'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { TextLength } from 'vs/editor/common/core/textLength'; + +/** + * Represents a list of mappings of ranges from one document to another. + */ +export class RangeMapping { + constructor(public readonly mappings: readonly SingleRangeMapping[]) { + } + + mapPosition(position: Position): PositionOrRange { + const mapping = findLastMonotonous(this.mappings, m => m.original.getStartPosition().isBeforeOrEqual(position)); + if (!mapping) { + return PositionOrRange.position(position); + } + if (mapping.original.containsPosition(position)) { + return PositionOrRange.range(mapping.modified); + } + const l = TextLength.betweenPositions(mapping.original.getEndPosition(), position); + return PositionOrRange.position(l.addToPosition(mapping.modified.getEndPosition())); + } + + mapRange(range: Range): Range { + const start = this.mapPosition(range.getStartPosition()); + const end = this.mapPosition(range.getEndPosition()); + return Range.fromPositions( + start.range?.getStartPosition() ?? start.position!, + end.range?.getEndPosition() ?? end.position!, + ); + } + + reverse(): RangeMapping { + return new RangeMapping(this.mappings.map(mapping => mapping.reverse())); + } +} + +export class SingleRangeMapping { + constructor( + public readonly original: Range, + public readonly modified: Range, + ) { + } + + reverse(): SingleRangeMapping { + return new SingleRangeMapping(this.modified, this.original); + } + + toString() { + return `${this.original.toString()} -> ${this.modified.toString()}`; + } +} + +export class PositionOrRange { + public static position(position: Position): PositionOrRange { + return new PositionOrRange(position, undefined); + } + + public static range(range: Range): PositionOrRange { + return new PositionOrRange(undefined, range); + } + + private constructor( + public readonly position: Position | undefined, + public readonly range: Range | undefined, + ) { } +} diff --git a/code/src/vs/editor/common/core/textEdit.ts b/code/src/vs/editor/common/core/textEdit.ts new file mode 100644 index 00000000000..e353361d953 --- /dev/null +++ b/code/src/vs/editor/common/core/textEdit.ts @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert, assertFn, checkAdjacentItems } from 'vs/base/common/assert'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Position } from 'vs/editor/common/core/position'; +import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; +import { Range } from 'vs/editor/common/core/range'; +import { TextLength } from 'vs/editor/common/core/textLength'; + +export class TextEdit { + constructor(public readonly edits: readonly SingleTextEdit[]) { + assertFn(() => checkAdjacentItems(edits, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition()))); + } + + /** + * Joins touching edits and removes empty edits. + */ + normalize(): TextEdit { + const edits: SingleTextEdit[] = []; + for (const edit of this.edits) { + if (edits.length > 0 && edits[edits.length - 1].range.getEndPosition().equals(edit.range.getStartPosition())) { + const last = edits[edits.length - 1]; + edits[edits.length - 1] = new SingleTextEdit(last.range.plusRange(edit.range), last.text + edit.text); + } else if (!edit.isEmpty) { + edits.push(edit); + } + } + return new TextEdit(edits); + } + + mapPosition(position: Position): Position | Range { + let lineDelta = 0; + let curLine = 0; + let columnDeltaInCurLine = 0; + + for (const edit of this.edits) { + const start = edit.range.getStartPosition(); + const end = edit.range.getEndPosition(); + + if (position.isBeforeOrEqual(start)) { + break; + } + + const len = TextLength.ofText(edit.text); + if (position.isBefore(end)) { + const startPos = new Position(start.lineNumber + lineDelta, start.column + (start.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0)); + const endPos = len.addToPosition(startPos); + return rangeFromPositions(startPos, endPos); + } + + lineDelta += len.lineCount - (edit.range.endLineNumber - edit.range.startLineNumber); + + if (len.lineCount === 0) { + if (end.lineNumber !== start.lineNumber) { + columnDeltaInCurLine += len.columnCount - (end.column - 1); + } else { + columnDeltaInCurLine += len.columnCount - (end.column - start.column); + } + } else { + columnDeltaInCurLine = len.columnCount; + } + curLine = end.lineNumber + lineDelta; + } + + return new Position(position.lineNumber + lineDelta, position.column + (position.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0)); + } + + mapRange(range: Range): Range { + function getStart(p: Position | Range) { + return p instanceof Position ? p : p.getStartPosition(); + } + + function getEnd(p: Position | Range) { + return p instanceof Position ? p : p.getEndPosition(); + } + + const start = getStart(this.mapPosition(range.getStartPosition())); + const end = getEnd(this.mapPosition(range.getEndPosition())); + + return rangeFromPositions(start, end); + } + + // TODO: `doc` is not needed for this! + inverseMapPosition(positionAfterEdit: Position, doc: AbstractText): Position | Range { + const reversed = this.inverse(doc); + return reversed.mapPosition(positionAfterEdit); + } + + inverseMapRange(range: Range, doc: AbstractText): Range { + const reversed = this.inverse(doc); + return reversed.mapRange(range); + } + + apply(text: AbstractText): string { + let result = ''; + let lastEditEnd = new Position(1, 1); + for (const edit of this.edits) { + const editRange = edit.range; + const editStart = editRange.getStartPosition(); + const editEnd = editRange.getEndPosition(); + + const r = rangeFromPositions(lastEditEnd, editStart); + if (!r.isEmpty()) { + result += text.getValueOfRange(r); + } + result += edit.text; + lastEditEnd = editEnd; + } + const r = rangeFromPositions(lastEditEnd, text.endPositionExclusive); + if (!r.isEmpty()) { + result += text.getValueOfRange(r); + } + return result; + } + + applyToString(str: string): string { + const strText = new StringText(str); + return this.apply(strText); + } + + inverse(doc: AbstractText): TextEdit { + const ranges = this.getNewRanges(); + return new TextEdit(this.edits.map((e, idx) => new SingleTextEdit(ranges[idx], doc.getValueOfRange(e.range)))); + } + + getNewRanges(): Range[] { + const newRanges: Range[] = []; + let previousEditEndLineNumber = 0; + let lineOffset = 0; + let columnOffset = 0; + for (const edit of this.edits) { + const textLength = TextLength.ofText(edit.text); + const newRangeStart = Position.lift({ + lineNumber: edit.range.startLineNumber + lineOffset, + column: edit.range.startColumn + (edit.range.startLineNumber === previousEditEndLineNumber ? columnOffset : 0) + }); + const newRange = textLength.createRange(newRangeStart); + newRanges.push(newRange); + lineOffset = newRange.endLineNumber - edit.range.endLineNumber; + columnOffset = newRange.endColumn - edit.range.endColumn; + previousEditEndLineNumber = edit.range.endLineNumber; + } + return newRanges; + } +} + +export class SingleTextEdit { + constructor( + public readonly range: Range, + public readonly text: string, + ) { + } + + get isEmpty(): boolean { + return this.range.isEmpty() && this.text.length === 0; + } + + static equals(first: SingleTextEdit, second: SingleTextEdit) { + return first.range.equalsRange(second.range) && first.text === second.text; + } +} + +function rangeFromPositions(start: Position, end: Position): Range { + if (!start.isBeforeOrEqual(end)) { + throw new BugIndicatingError('start must be before end'); + } + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); +} + +export abstract class AbstractText { + abstract getValueOfRange(range: Range): string; + abstract readonly length: TextLength; + + get endPositionExclusive(): Position { + return this.length.addToPosition(new Position(1, 1)); + } + + getValue() { + return this.getValueOfRange(this.length.toRange()); + } +} + +export class LineBasedText extends AbstractText { + constructor( + private readonly _getLineContent: (lineNumber: number) => string, + private readonly _lineCount: number, + ) { + assert(_lineCount >= 1); + + super(); + } + + getValueOfRange(range: Range): string { + if (range.startLineNumber === range.endLineNumber) { + return this._getLineContent(range.startLineNumber).substring(range.startColumn - 1, range.endColumn - 1); + } + let result = this._getLineContent(range.startLineNumber).substring(range.startColumn - 1); + for (let i = range.startLineNumber + 1; i < range.endLineNumber; i++) { + result += '\n' + this._getLineContent(i); + } + result += '\n' + this._getLineContent(range.endLineNumber).substring(0, range.endColumn - 1); + return result; + } + + get length(): TextLength { + const lastLine = this._getLineContent(this._lineCount); + return new TextLength(this._lineCount - 1, lastLine.length); + } +} + +export class StringText extends AbstractText { + private readonly _t = new PositionOffsetTransformer(this.value); + + constructor(public readonly value: string) { + super(); + } + + getValueOfRange(range: Range): string { + return this._t.getOffsetRange(range).substring(this.value); + } + + get length(): TextLength { + return this._t.textLength; + } +} diff --git a/code/src/vs/editor/common/core/textLength.ts b/code/src/vs/editor/common/core/textLength.ts new file mode 100644 index 00000000000..632895c55fd --- /dev/null +++ b/code/src/vs/editor/common/core/textLength.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; + +/** + * Represents a non-negative length of text in terms of line and column count. +*/ +export class TextLength { + public static zero = new TextLength(0, 0); + + public static lengthDiffNonNegative(start: TextLength, end: TextLength): TextLength { + if (end.isLessThan(start)) { + return TextLength.zero; + } + if (start.lineCount === end.lineCount) { + return new TextLength(0, end.columnCount - start.columnCount); + } else { + return new TextLength(end.lineCount - start.lineCount, end.columnCount); + } + } + + public static betweenPositions(position1: Position, position2: Position): TextLength { + if (position1.lineNumber === position2.lineNumber) { + return new TextLength(0, position2.column - position1.column); + } else { + return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1); + } + } + + public static ofRange(range: Range) { + return TextLength.betweenPositions(range.getStartPosition(), range.getEndPosition()); + } + + public static ofText(text: string): TextLength { + let line = 0; + let column = 0; + for (const c of text) { + if (c === '\n') { + line++; + column = 0; + } else { + column++; + } + } + return new TextLength(line, column); + } + + constructor( + public readonly lineCount: number, + public readonly columnCount: number + ) { } + + public isZero() { + return this.lineCount === 0 && this.columnCount === 0; + } + + public isLessThan(other: TextLength): boolean { + if (this.lineCount !== other.lineCount) { + return this.lineCount < other.lineCount; + } + return this.columnCount < other.columnCount; + } + + public isGreaterThan(other: TextLength): boolean { + if (this.lineCount !== other.lineCount) { + return this.lineCount > other.lineCount; + } + return this.columnCount > other.columnCount; + } + + public equals(other: TextLength): boolean { + return this.lineCount === other.lineCount && this.columnCount === other.columnCount; + } + + public compare(other: TextLength): number { + if (this.lineCount !== other.lineCount) { + return this.lineCount - other.lineCount; + } + return this.columnCount - other.columnCount; + } + + public add(other: TextLength): TextLength { + if (other.lineCount === 0) { + return new TextLength(this.lineCount, this.columnCount + other.columnCount); + } else { + return new TextLength(this.lineCount + other.lineCount, other.columnCount); + } + } + + public createRange(startPosition: Position): Range { + if (this.lineCount === 0) { + return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column + this.columnCount); + } else { + return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber + this.lineCount, this.columnCount + 1); + } + } + + public toRange(): Range { + return new Range(1, 1, this.lineCount + 1, this.columnCount + 1); + } + + public addToPosition(position: Position): Position { + if (this.lineCount === 0) { + return new Position(position.lineNumber, position.column + this.columnCount); + } else { + return new Position(position.lineNumber + this.lineCount, this.columnCount + 1); + } + } + + toString() { + return `${this.lineCount},${this.columnCount}`; + } +} diff --git a/code/src/vs/editor/common/core/wordCharacterClassifier.ts b/code/src/vs/editor/common/core/wordCharacterClassifier.ts index 638ff3ac26a..b984c272657 100644 --- a/code/src/vs/editor/common/core/wordCharacterClassifier.ts +++ b/code/src/vs/editor/common/core/wordCharacterClassifier.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CharCode } from 'vs/base/common/charCode'; +import { LRUCache } from 'vs/base/common/map'; import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; export const enum WordCharacterClass { @@ -14,8 +15,19 @@ export const enum WordCharacterClass { export class WordCharacterClassifier extends CharacterClassifier { - constructor(wordSeparators: string) { + public readonly intlSegmenterLocales: Intl.UnicodeBCP47LocaleIdentifier[]; + private readonly _segmenter: Intl.Segmenter | null = null; + private _cachedLine: string | null = null; + private _cachedSegments: IntlWordSegmentData[] = []; + + constructor(wordSeparators: string, intlSegmenterLocales: Intl.UnicodeBCP47LocaleIdentifier[]) { super(WordCharacterClass.Regular); + this.intlSegmenterLocales = intlSegmenterLocales; + if (this.intlSegmenterLocales.length > 0) { + this._segmenter = new Intl.Segmenter(this.intlSegmenterLocales, { granularity: 'word' }); + } else { + this._segmenter = null; + } for (let i = 0, len = wordSeparators.length; i < len; i++) { this.set(wordSeparators.charCodeAt(i), WordCharacterClass.WordSeparator); @@ -25,18 +37,74 @@ export class WordCharacterClassifier extends CharacterClassifier offset) { + break; + } + candidate = segment; + } + return candidate; + } + + public findNextIntlWordAtOrAfterOffset(lineContent: string, offset: number): IntlWordSegmentData | null { + for (const segment of this._getIntlSegmenterWordsOnLine(lineContent)) { + if (segment.index < offset) { + continue; + } + return segment; + } + return null; + } + + private _getIntlSegmenterWordsOnLine(line: string): IntlWordSegmentData[] { + if (!this._segmenter) { + return []; + } + + // Check if the line has changed from the previous call + if (this._cachedLine === line) { + return this._cachedSegments; + } -function once(computeFn: (input: string) => R): (input: string) => R { - const cache: { [key: string]: R } = {}; // TODO@Alex unbounded cache - return (input: string): R => { - if (!cache.hasOwnProperty(input)) { - cache[input] = computeFn(input); + // Update the cache with the new line + this._cachedLine = line; + this._cachedSegments = this._filterWordSegments(this._segmenter.segment(line)); + + return this._cachedSegments; + } + + private _filterWordSegments(segments: Intl.Segments): IntlWordSegmentData[] { + const result: IntlWordSegmentData[] = []; + for (const segment of segments) { + if (this._isWordLike(segment)) { + result.push(segment); + } + } + return result; + } + + private _isWordLike(segment: Intl.SegmentData): segment is IntlWordSegmentData { + if (segment.isWordLike) { + return true; } - return cache[input]; - }; + return false; + } +} + +export interface IntlWordSegmentData extends Intl.SegmentData { + isWordLike: true; } -export const getMapForWordSeparators = once( - (input) => new WordCharacterClassifier(input) -); +const wordClassifierCache = new LRUCache(10); + +export function getMapForWordSeparators(wordSeparators: string, intlSegmenterLocales: Intl.UnicodeBCP47LocaleIdentifier[]): WordCharacterClassifier { + const key = `${wordSeparators}/${intlSegmenterLocales.join(',')}`; + let result = wordClassifierCache.get(key)!; + if (!result) { + result = new WordCharacterClassifier(wordSeparators, intlSegmenterLocales); + wordClassifierCache.set(key, result); + } + return result; +} diff --git a/code/src/vs/editor/common/cursor/cursorTypeOperations.ts b/code/src/vs/editor/common/cursor/cursorTypeOperations.ts index e71f02a960e..ffa80cbb63c 100644 --- a/code/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/code/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -648,7 +648,7 @@ export class TypeOperations { // Do not auto-close ' or " after a word character if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { - const wordSeparators = getMapForWordSeparators(config.wordSeparators); + const wordSeparators = getMapForWordSeparators(config.wordSeparators, []); if (lineBefore.length > 0) { const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1); if (wordSeparators.get(characterBefore) === WordCharacterClass.Regular) { diff --git a/code/src/vs/editor/common/cursor/cursorWordOperations.ts b/code/src/vs/editor/common/cursor/cursorWordOperations.ts index 8a3f98d37c2..b16172cc89a 100644 --- a/code/src/vs/editor/common/cursor/cursorWordOperations.ts +++ b/code/src/vs/editor/common/cursor/cursorWordOperations.ts @@ -8,7 +8,7 @@ import * as strings from 'vs/base/common/strings'; import { EditorAutoClosingEditStrategy, EditorAutoClosingStrategy } from 'vs/editor/common/config/editorOptions'; import { CursorConfiguration, ICursorSimpleModel, SelectionStartKind, SingleCursorState } from 'vs/editor/common/cursorCommon'; import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations'; -import { WordCharacterClass, WordCharacterClassifier, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; +import { WordCharacterClass, WordCharacterClassifier, IntlWordSegmentData, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -67,6 +67,11 @@ export class WordOperations { return { start: start, end: end, wordType: wordType, nextCharClass: nextCharClass }; } + private static _createIntlWord(intlWord: IntlWordSegmentData, nextCharClass: WordCharacterClass): IFindWordResult { + // console.log('INTL WORD ==> ' + intlWord.index + ' => ' + intlWord.index + intlWord.segment.length + ':::: <<<' + intlWord.segment + '>>>'); + return { start: intlWord.index, end: intlWord.index + intlWord.segment.length, wordType: WordType.Regular, nextCharClass: nextCharClass }; + } + private static _findPreviousWordOnLine(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): IFindWordResult | null { const lineContent = model.getLineContent(position.lineNumber); return this._doFindPreviousWordOnLine(lineContent, wordSeparators, position); @@ -74,10 +79,17 @@ export class WordOperations { private static _doFindPreviousWordOnLine(lineContent: string, wordSeparators: WordCharacterClassifier, position: Position): IFindWordResult | null { let wordType = WordType.None; + + const previousIntlWord = wordSeparators.findPrevIntlWordBeforeOrAtOffset(lineContent, position.column - 2); + for (let chIndex = position.column - 2; chIndex >= 0; chIndex--) { const chCode = lineContent.charCodeAt(chIndex); const chClass = wordSeparators.get(chCode); + if (previousIntlWord && chIndex === previousIntlWord.index) { + return this._createIntlWord(previousIntlWord, chClass); + } + if (chClass === WordCharacterClass.Regular) { if (wordType === WordType.Separator) { return this._createWord(lineContent, wordType, chClass, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1)); @@ -103,11 +115,18 @@ export class WordOperations { } private static _findEndOfWord(lineContent: string, wordSeparators: WordCharacterClassifier, wordType: WordType, startIndex: number): number { + + const nextIntlWord = wordSeparators.findNextIntlWordAtOrAfterOffset(lineContent, startIndex); + const len = lineContent.length; for (let chIndex = startIndex; chIndex < len; chIndex++) { const chCode = lineContent.charCodeAt(chIndex); const chClass = wordSeparators.get(chCode); + if (nextIntlWord && chIndex === nextIntlWord.index + nextIntlWord.segment.length) { + return chIndex; + } + if (chClass === WordCharacterClass.Whitespace) { return chIndex; } @@ -130,10 +149,16 @@ export class WordOperations { let wordType = WordType.None; const len = lineContent.length; + const nextIntlWord = wordSeparators.findNextIntlWordAtOrAfterOffset(lineContent, position.column - 1); + for (let chIndex = position.column - 1; chIndex < len; chIndex++) { const chCode = lineContent.charCodeAt(chIndex); const chClass = wordSeparators.get(chCode); + if (nextIntlWord && chIndex === nextIntlWord.index) { + return this._createIntlWord(nextIntlWord, chClass); + } + if (chClass === WordCharacterClass.Regular) { if (wordType === WordType.Separator) { return this._createWord(lineContent, wordType, chClass, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex); @@ -159,10 +184,17 @@ export class WordOperations { } private static _findStartOfWord(lineContent: string, wordSeparators: WordCharacterClassifier, wordType: WordType, startIndex: number): number { + + const previousIntlWord = wordSeparators.findPrevIntlWordBeforeOrAtOffset(lineContent, startIndex); + for (let chIndex = startIndex; chIndex >= 0; chIndex--) { const chCode = lineContent.charCodeAt(chIndex); const chClass = wordSeparators.get(chCode); + if (previousIntlWord && chIndex === previousIntlWord.index) { + return chIndex; + } + if (chClass === WordCharacterClass.Whitespace) { return chIndex + 1; } @@ -689,8 +721,8 @@ export class WordOperations { }; } - public static getWordAtPosition(model: ITextModel, _wordSeparators: string, position: Position): IWordAtPosition | null { - const wordSeparators = getMapForWordSeparators(_wordSeparators); + public static getWordAtPosition(model: ITextModel, _wordSeparators: string, _intlSegmenterLocales: string[], position: Position): IWordAtPosition | null { + const wordSeparators = getMapForWordSeparators(_wordSeparators, _intlSegmenterLocales); const prevWord = WordOperations._findPreviousWordOnLine(wordSeparators, model, position); if (prevWord && prevWord.wordType === WordType.Regular && prevWord.start <= position.column - 1 && position.column - 1 <= prevWord.end) { return WordOperations._createWordAtPosition(model, position.lineNumber, prevWord); @@ -703,7 +735,7 @@ export class WordOperations { } public static word(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, position: Position): SingleCursorState { - const wordSeparators = getMapForWordSeparators(config.wordSeparators); + const wordSeparators = getMapForWordSeparators(config.wordSeparators, config.wordSegmenterLocales); const prevWord = WordOperations._findPreviousWordOnLine(wordSeparators, model, position); const nextWord = WordOperations._findNextWordOnLine(wordSeparators, model, position); diff --git a/code/src/vs/editor/common/cursorCommon.ts b/code/src/vs/editor/common/cursorCommon.ts index 13b95ad1299..c5411aa8539 100644 --- a/code/src/vs/editor/common/cursorCommon.ts +++ b/code/src/vs/editor/common/cursorCommon.ts @@ -76,6 +76,7 @@ export class CursorConfiguration { public readonly surroundingPairs: CharacterMap; public readonly blockCommentStartToken: string | null; public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean; bracket: (ch: string) => boolean; comment: (ch: string) => boolean }; + public readonly wordSegmenterLocales: string[]; private readonly _languageId: string; private _electricChars: { [key: string]: boolean } | null; @@ -97,6 +98,7 @@ export class CursorConfiguration { || e.hasChanged(EditorOption.useTabStops) || e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.readOnly) + || e.hasChanged(EditorOption.wordSegmenterLocales) ); } @@ -134,6 +136,7 @@ export class CursorConfiguration { this.autoClosingOvertype = options.get(EditorOption.autoClosingOvertype); this.autoSurround = options.get(EditorOption.autoSurround); this.autoIndent = options.get(EditorOption.autoIndent); + this.wordSegmenterLocales = options.get(EditorOption.wordSegmenterLocales); this.surroundingPairs = {}; this._electricChars = null; diff --git a/code/src/vs/editor/common/diff/rangeMapping.ts b/code/src/vs/editor/common/diff/rangeMapping.ts index d00f0061698..1d8b154367e 100644 --- a/code/src/vs/editor/common/diff/rangeMapping.ts +++ b/code/src/vs/editor/common/diff/rangeMapping.ts @@ -92,6 +92,12 @@ export class LineRangeMapping { * Also contains inner range mappings. */ export class DetailedLineRangeMapping extends LineRangeMapping { + public static fromRangeMappings(rangeMappings: RangeMapping[]): DetailedLineRangeMapping { + const originalRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.originalRange))); + const modifiedRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.modifiedRange))); + return new DetailedLineRangeMapping(originalRange, modifiedRange, rangeMappings); + } + /** * If inner changes have not been computed, this is set to undefined. * Otherwise, it represents the character-level diff in this line range. diff --git a/code/src/vs/editor/common/editorCommon.ts b/code/src/vs/editor/common/editorCommon.ts index 480c1250bae..57b9cde6268 100644 --- a/code/src/vs/editor/common/editorCommon.ts +++ b/code/src/vs/editor/common/editorCommon.ts @@ -771,12 +771,3 @@ export interface CompositionTypePayload { positionDelta: number; } -/** - * @internal - */ -export interface PastePayload { - text: string; - pasteOnNewLine: boolean; - multicursorText: string[] | null; - mode: string | null; -} diff --git a/code/src/vs/editor/common/editorContextKeys.ts b/code/src/vs/editor/common/editorContextKeys.ts index a38c21c4717..2311fbd3a85 100644 --- a/code/src/vs/editor/common/editorContextKeys.ts +++ b/code/src/vs/editor/common/editorContextKeys.ts @@ -30,10 +30,16 @@ export namespace EditorContextKeys { export const inMultiDiffEditor = new RawContextKey('inMultiDiffEditor', false, nls.localize('inMultiDiffEditor', "Whether the context is a multi diff editor")); export const multiDiffEditorAllCollapsed = new RawContextKey('multiDiffEditorAllCollapsed', undefined, nls.localize('multiDiffEditorAllCollapsed', "Whether all files in multi diff editor are collapsed")); export const hasChanges = new RawContextKey('diffEditorHasChanges', false, nls.localize('diffEditorHasChanges', "Whether the diff editor has changes")); - export const comparingMovedCode = new RawContextKey('comparingMovedCode', false, nls.localize('comparingMovedCode', "Whether a moved code block is selected for comparison")); export const accessibleDiffViewerVisible = new RawContextKey('accessibleDiffViewerVisible', false, nls.localize('accessibleDiffViewerVisible', "Whether the accessible diff viewer is visible")); export const diffEditorRenderSideBySideInlineBreakpointReached = new RawContextKey('diffEditorRenderSideBySideInlineBreakpointReached', false, nls.localize('diffEditorRenderSideBySideInlineBreakpointReached', "Whether the diff editor render side by side inline breakpoint is reached")); + export const diffEditorInlineMode = new RawContextKey('diffEditorInlineMode', false, nls.localize('diffEditorInlineMode', "Whether inline mode is active")); + + export const diffEditorOriginalWritable = new RawContextKey('diffEditorOriginalWritable', false, nls.localize('diffEditorOriginalWritable', "Whether modified is writable in the diff editor")); + export const diffEditorModifiedWritable = new RawContextKey('diffEditorModifiedWritable', false, nls.localize('diffEditorModifiedWritable', "Whether modified is writable in the diff editor")); + export const diffEditorOriginalUri = new RawContextKey('diffEditorOriginalUri', '', nls.localize('diffEditorOriginalUri', "The uri of the original document")); + export const diffEditorModifiedUri = new RawContextKey('diffEditorModifiedUri', '', nls.localize('diffEditorModifiedUri', "The uri of the modified document")); + export const columnSelection = new RawContextKey('editorColumnSelection', false, nls.localize('editorColumnSelection', "Whether `editor.columnSelection` is enabled")); export const writable = readOnly.toNegated(); export const hasNonEmptySelection = new RawContextKey('editorHasSelection', false, nls.localize('editorHasSelection', "Whether the editor has text selected")); diff --git a/code/src/vs/editor/common/languages.ts b/code/src/vs/editor/common/languages.ts index 16550bdef8d..2c3925b4d45 100644 --- a/code/src/vs/editor/common/languages.ts +++ b/code/src/vs/editor/common/languages.ts @@ -25,6 +25,7 @@ import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IMarkerData } from 'vs/platform/markers/common/markers'; import { LanguageFilter } from 'vs/editor/common/languageSelector'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; /** * @internal @@ -547,6 +548,22 @@ export interface CompletionList { duration?: number; } +/** + * Info provided on partial acceptance. + */ +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +/** + * How a partial acceptance was triggered. + */ +export const enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2, +} + /** * How a suggest provider was triggered. */ @@ -718,7 +735,7 @@ export interface InlineCompletionsProvider; - provideDocumentPasteEdits?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise; + provideDocumentPasteEdits?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise; + + resolveDocumentPasteEdit?(edit: DocumentPasteEdit, token: CancellationToken): Promise; } /** @@ -1710,6 +1744,14 @@ export interface CommentInfo { commentingRanges: CommentingRanges; } + +/** + * @internal + */ +export interface CommentingRangeResourceHint { + schemes: readonly string[]; +} + /** * @internal */ @@ -1732,6 +1774,14 @@ export enum CommentThreadState { Resolved = 1 } +/** + * @internal + */ +export enum CommentThreadApplicability { + Current = 0, + Outdated = 1 +} + /** * @internal */ @@ -1769,6 +1819,7 @@ export interface CommentThread { initialCollapsibleState?: CommentThreadCollapsibleState; onDidChangeInitialCollapsibleState: Event; state?: CommentThreadState; + applicability?: CommentThreadApplicability; canReply: boolean; input?: CommentInput; onDidChangeInput: Event; @@ -1859,7 +1910,7 @@ export interface PendingCommentThread { body: string; range: IRange | undefined; uri: URI; - owner: string; + uniqueOwner: string; isReply: boolean; } @@ -2090,13 +2141,14 @@ export enum ExternalUriOpenerPriority { /** * @internal */ -export type DropYieldTo = { readonly providerId: string } | { readonly mimeType: string }; +export type DropYieldTo = { readonly kind: HierarchicalKind } | { readonly mimeType: string }; /** * @internal */ export interface DocumentOnDropEdit { - readonly label: string; + readonly title: string; + readonly kind: HierarchicalKind | undefined; readonly handledMimeType?: string; readonly yieldTo?: readonly DropYieldTo[]; insertText: string | { readonly snippet: string }; @@ -2110,7 +2162,7 @@ export interface DocumentOnDropEditProvider { readonly id?: string; readonly dropMimeTypes?: readonly string[]; - provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; + provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; } export interface DocumentContextItem { diff --git a/code/src/vs/editor/common/model.ts b/code/src/vs/editor/common/model.ts index ad0d31765ff..e21aa7d600c 100644 --- a/code/src/vs/editor/common/model.ts +++ b/code/src/vs/editor/common/model.ts @@ -70,11 +70,19 @@ export interface IGlyphMarginLanesModel { /** * Position in the minimap to render the decoration. */ -export enum MinimapPosition { +export const enum MinimapPosition { Inline = 1, Gutter = 2 } +/** + * Section header style. + */ +export const enum MinimapSectionHeaderStyle { + Normal = 1, + Underlined = 2 +} + export interface IDecorationOptions { /** * CSS color to render. @@ -119,6 +127,14 @@ export interface IModelDecorationMinimapOptions extends IDecorationOptions { * The position in the minimap. */ position: MinimapPosition; + /** + * If the decoration is for a section header, which header style. + */ + sectionHeaderStyle?: MinimapSectionHeaderStyle | null; + /** + * If the decoration is for a section header, the header text. + */ + sectionHeaderText?: string | null; } /** diff --git a/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts b/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts index 501aa07c39b..1f95f84df48 100644 --- a/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts +++ b/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Range } from 'vs/editor/common/core/range'; -import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, LengthObj, lengthOfString, lengthToObj, positionToLength, toLength } from './length'; +import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, lengthOfString, lengthToObj, positionToLength, toLength } from './length'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { IModelContentChange } from 'vs/editor/common/textModelEvents'; export class TextEditInfo { @@ -73,7 +74,7 @@ export class BeforeEditPositionMapper { return lengthDiffNonNegative(offset, nextChangeOffset); } - private translateOldToCur(oldOffsetObj: LengthObj): Length { + private translateOldToCur(oldOffsetObj: TextLength): Length { if (oldOffsetObj.lineCount === this.deltaLineIdxInOld) { return toLength(oldOffsetObj.lineCount + this.deltaOldToNewLineCount, oldOffsetObj.columnCount + this.deltaOldToNewColumnCount); } else { @@ -126,9 +127,9 @@ class TextEditInfoCache { return new TextEditInfoCache(edit.startOffset, edit.endOffset, edit.newLength); } - public readonly endOffsetBeforeObj: LengthObj; - public readonly endOffsetAfterObj: LengthObj; - public readonly offsetObj: LengthObj; + public readonly endOffsetBeforeObj: TextLength; + public readonly endOffsetAfterObj: TextLength; + public readonly offsetObj: TextLength; constructor( startOffset: Length, diff --git a/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts b/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts index 40cb0255688..d41a62233e5 100644 --- a/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts +++ b/code/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts @@ -6,75 +6,7 @@ import { splitLines } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; - -/** - * Represents a non-negative length in terms of line and column count. - * Prefer using {@link Length} for performance reasons. -*/ -export class LengthObj { - public static zero = new LengthObj(0, 0); - - public static lengthDiffNonNegative(start: LengthObj, end: LengthObj): LengthObj { - if (end.isLessThan(start)) { - return LengthObj.zero; - } - if (start.lineCount === end.lineCount) { - return new LengthObj(0, end.columnCount - start.columnCount); - } else { - return new LengthObj(end.lineCount - start.lineCount, end.columnCount); - } - } - - constructor( - public readonly lineCount: number, - public readonly columnCount: number - ) { } - - public isZero() { - return this.lineCount === 0 && this.columnCount === 0; - } - - public toLength(): Length { - return toLength(this.lineCount, this.columnCount); - } - - public isLessThan(other: LengthObj): boolean { - if (this.lineCount !== other.lineCount) { - return this.lineCount < other.lineCount; - } - return this.columnCount < other.columnCount; - } - - public isGreaterThan(other: LengthObj): boolean { - if (this.lineCount !== other.lineCount) { - return this.lineCount > other.lineCount; - } - return this.columnCount > other.columnCount; - } - - public equals(other: LengthObj): boolean { - return this.lineCount === other.lineCount && this.columnCount === other.columnCount; - } - - public compare(other: LengthObj): number { - if (this.lineCount !== other.lineCount) { - return this.lineCount - other.lineCount; - } - return this.columnCount - other.columnCount; - } - - public add(other: LengthObj): LengthObj { - if (other.lineCount === 0) { - return new LengthObj(this.lineCount, this.columnCount + other.columnCount); - } else { - return new LengthObj(this.lineCount + other.lineCount, other.columnCount); - } - } - - toString() { - return `${this.lineCount},${this.columnCount}`; - } -} +import { TextLength } from 'vs/editor/common/core/textLength'; /** * The end must be greater than or equal to the start. @@ -117,11 +49,11 @@ export function toLength(lineCount: number, columnCount: number): Length { return (lineCount * factor + columnCount) as any as Length; } -export function lengthToObj(length: Length): LengthObj { +export function lengthToObj(length: Length): TextLength { const l = length as any as number; const lineCount = Math.floor(l / factor); const columnCount = l - lineCount * factor; - return new LengthObj(lineCount, columnCount); + return new TextLength(lineCount, columnCount); } export function lengthGetLineCount(length: Length): number { @@ -216,11 +148,11 @@ export function lengthsToRange(lengthStart: Length, lengthEnd: Length): Range { return new Range(lineCount + 1, colCount + 1, lineCount2 + 1, colCount2 + 1); } -export function lengthOfRange(range: Range): LengthObj { +export function lengthOfRange(range: Range): TextLength { if (range.startLineNumber === range.endLineNumber) { - return new LengthObj(0, range.endColumn - range.startColumn); + return new TextLength(0, range.endColumn - range.startColumn); } else { - return new LengthObj(range.endLineNumber - range.startLineNumber, range.endColumn - 1); + return new TextLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); } } @@ -235,9 +167,9 @@ export function lengthOfString(str: string): Length { return toLength(lines.length - 1, lines[lines.length - 1].length); } -export function lengthOfStringObj(str: string): LengthObj { +export function lengthOfStringObj(str: string): TextLength { const lines = splitLines(str); - return new LengthObj(lines.length - 1, lines[lines.length - 1].length); + return new TextLength(lines.length - 1, lines[lines.length - 1].length); } /** diff --git a/code/src/vs/editor/common/model/textModel.ts b/code/src/vs/editor/common/model/textModel.ts index 7117b8240af..a3e282ba628 100644 --- a/code/src/vs/editor/common/model/textModel.ts +++ b/code/src/vs/editor/common/model/textModel.ts @@ -1256,7 +1256,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati ); } - private _validateEditOperations(rawOperations: model.IIdentifiedSingleEditOperation[]): model.ValidAnnotatedEditOperation[] { + private _validateEditOperations(rawOperations: readonly model.IIdentifiedSingleEditOperation[]): model.ValidAnnotatedEditOperation[] { const result: model.ValidAnnotatedEditOperation[] = []; for (let i = 0, len = rawOperations.length; i < len; i++) { result[i] = this._validateEditOperation(rawOperations[i]); @@ -1406,10 +1406,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } } - public applyEdits(operations: model.IIdentifiedSingleEditOperation[]): void; - public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: false): void; - public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: true): model.IValidEditOperation[]; - public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = false): void | model.IValidEditOperation[] { + public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[]): void; + public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: false): void; + public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: true): model.IValidEditOperation[]; + public applyEdits(rawOperations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = false): void | model.IValidEditOperation[] { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); @@ -2219,12 +2219,15 @@ export class ModelDecorationGlyphMarginOptions { export class ModelDecorationMinimapOptions extends DecorationOptions { readonly position: model.MinimapPosition; + readonly sectionHeaderStyle: model.MinimapSectionHeaderStyle | null; + readonly sectionHeaderText: string | null; private _resolvedColor: Color | undefined; - constructor(options: model.IModelDecorationMinimapOptions) { super(options); this.position = options.position; + this.sectionHeaderStyle = options.sectionHeaderStyle ?? null; + this.sectionHeaderText = options.sectionHeaderText ?? null; } public getColor(theme: IColorTheme): Color | undefined { diff --git a/code/src/vs/editor/common/model/textModelSearch.ts b/code/src/vs/editor/common/model/textModelSearch.ts index 87c4bd8ecf7..81f6cbc5e20 100644 --- a/code/src/vs/editor/common/model/textModelSearch.ts +++ b/code/src/vs/editor/common/model/textModelSearch.ts @@ -62,7 +62,7 @@ export class SearchParams { canUseSimpleSearch = this.matchCase; } - return new SearchData(regex, this.wordSeparators ? getMapForWordSeparators(this.wordSeparators) : null, canUseSimpleSearch ? this.searchString : null); + return new SearchData(regex, this.wordSeparators ? getMapForWordSeparators(this.wordSeparators, []) : null, canUseSimpleSearch ? this.searchString : null); } } diff --git a/code/src/vs/editor/common/model/textModelText.ts b/code/src/vs/editor/common/model/textModelText.ts new file mode 100644 index 00000000000..0a603fa1ed2 --- /dev/null +++ b/code/src/vs/editor/common/model/textModelText.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from 'vs/editor/common/core/range'; +import { AbstractText } from 'vs/editor/common/core/textEdit'; +import { TextLength } from 'vs/editor/common/core/textLength'; +import { ITextModel } from 'vs/editor/common/model'; + +export class TextModelText extends AbstractText { + constructor(private readonly _textModel: ITextModel) { + super(); + } + + getValueOfRange(range: Range): string { + return this._textModel.getValueInRange(range); + } + + get length(): TextLength { + const lastLineNumber = this._textModel.getLineCount(); + const lastLineLen = this._textModel.getLineLength(lastLineNumber); + return new TextLength(lastLineNumber - 1, lastLineLen); + } +} diff --git a/code/src/vs/editor/common/model/textModelTokens.ts b/code/src/vs/editor/common/model/textModelTokens.ts index fdfb6dbe98f..fb1b7364d49 100644 --- a/code/src/vs/editor/common/model/textModelTokens.ts +++ b/code/src/vs/editor/common/model/textModelTokens.ts @@ -128,6 +128,11 @@ export class TokenizerWithStateStoreAndTextModel return lineTokens; } + public hasAccurateTokensForLine(lineNumber: number): boolean { + const firstInvalidLineNumber = this.store.getFirstInvalidEndStateLineNumberOrMax(); + return (lineNumber < firstInvalidLineNumber); + } + public isCheapToTokenize(lineNumber: number): boolean { const firstInvalidLineNumber = this.store.getFirstInvalidEndStateLineNumberOrMax(); if (lineNumber < firstInvalidLineNumber) { diff --git a/code/src/vs/editor/common/model/tokenizationTextModelPart.ts b/code/src/vs/editor/common/model/tokenizationTextModelPart.ts index 61490912068..804f63c6a28 100644 --- a/code/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/code/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -142,6 +142,11 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz this.grammarTokens.forceTokenization(lineNumber); } + public hasAccurateTokensForLine(lineNumber: number): boolean { + this.validateLineNumber(lineNumber); + return this.grammarTokens.hasAccurateTokensForLine(lineNumber); + } + public isCheapToTokenize(lineNumber: number): boolean { this.validateLineNumber(lineNumber); return this.grammarTokens.isCheapToTokenize(lineNumber); @@ -568,6 +573,13 @@ class GrammarTokens extends Disposable { this._defaultBackgroundTokenizer?.checkFinished(); } + public hasAccurateTokensForLine(lineNumber: number): boolean { + if (!this._tokenizer) { + return true; + } + return this._tokenizer.hasAccurateTokensForLine(lineNumber); + } + public isCheapToTokenize(lineNumber: number): boolean { if (!this._tokenizer) { return true; diff --git a/code/src/vs/editor/common/services/editorSimpleWorker.ts b/code/src/vs/editor/common/services/editorSimpleWorker.ts index f03e018cac0..195a870b0af 100644 --- a/code/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/code/src/vs/editor/common/services/editorSimpleWorker.ts @@ -28,6 +28,7 @@ import { createProxyObject, getAllMethodNames } from 'vs/base/common/objects'; import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider'; import { BugIndicatingError } from 'vs/base/common/errors'; import { IDocumentColorComputerTarget, computeDefaultDocumentColors } from 'vs/editor/common/languages/defaultDocumentColorsComputer'; +import { FindSectionHeaderOptions, SectionHeader, findSectionHeaders } from 'vs/editor/common/services/findSectionHeaders'; export interface IMirrorModel extends IMirrorTextModel { readonly uri: URI; @@ -401,6 +402,14 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range); } + public async findSectionHeaders(url: string, options: FindSectionHeaderOptions): Promise { + const model = this._getModel(url); + if (!model) { + return []; + } + return findSectionHeaders(model, options); + } + // ---- BEGIN diff -------------------------------------------------------------------------- public async computeDiff(originalUrl: string, modifiedUrl: string, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise { diff --git a/code/src/vs/editor/common/services/editorWorker.ts b/code/src/vs/editor/common/services/editorWorker.ts index 9e1cca8a460..7e87024cafc 100644 --- a/code/src/vs/editor/common/services/editorWorker.ts +++ b/code/src/vs/editor/common/services/editorWorker.ts @@ -11,6 +11,7 @@ import { IInplaceReplaceSupportResult, TextEdit } from 'vs/editor/common/languag import { UnicodeHighlighterOptions } from 'vs/editor/common/services/unicodeTextModelHighlighter'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import type { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; +import { SectionHeader, FindSectionHeaderOptions } from 'vs/editor/common/services/findSectionHeaders'; export const IEditorWorkerService = createDecorator('editorWorkerService'); @@ -36,6 +37,8 @@ export interface IEditorWorkerService { canNavigateValueSet(resource: URI): boolean; navigateValueSet(resource: URI, range: IRange, up: boolean): Promise; + + findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise; } export interface IDiffComputationResult { diff --git a/code/src/vs/editor/common/services/findSectionHeaders.ts b/code/src/vs/editor/common/services/findSectionHeaders.ts new file mode 100644 index 00000000000..08bd3709741 --- /dev/null +++ b/code/src/vs/editor/common/services/findSectionHeaders.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRange } from 'vs/editor/common/core/range'; +import { FoldingRules } from 'vs/editor/common/languages/languageConfiguration'; + +export interface ISectionHeaderFinderTarget { + getLineCount(): number; + getLineContent(lineNumber: number): string; +} + +export interface FindSectionHeaderOptions { + foldingRules?: FoldingRules; + findRegionSectionHeaders: boolean; + findMarkSectionHeaders: boolean; +} + +export interface SectionHeader { + /** + * The location of the header text in the text model. + */ + range: IRange; + /** + * The section header text. + */ + text: string; + /** + * Whether the section header includes a separator line. + */ + hasSeparatorLine: boolean; + /** + * This section should be omitted before rendering if it's not in a comment. + */ + shouldBeInComments: boolean; +} + +const markRegex = /\bMARK:\s*(.*)$/d; +const trimDashesRegex = /^-+|-+$/g; + +/** + * Find section headers in the model. + * + * @param model the text model to search in + * @param options options to search with + * @returns an array of section headers + */ +export function findSectionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] { + let headers: SectionHeader[] = []; + if (options.findRegionSectionHeaders && options.foldingRules?.markers) { + const regionHeaders = collectRegionHeaders(model, options); + headers = headers.concat(regionHeaders); + } + if (options.findMarkSectionHeaders) { + const markHeaders = collectMarkHeaders(model); + headers = headers.concat(markHeaders); + } + return headers; +} + +function collectRegionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] { + const regionHeaders: SectionHeader[] = []; + const endLineNumber = model.getLineCount(); + for (let lineNumber = 1; lineNumber <= endLineNumber; lineNumber++) { + const lineContent = model.getLineContent(lineNumber); + const match = lineContent.match(options.foldingRules!.markers!.start); + if (match) { + const range = { startLineNumber: lineNumber, startColumn: match[0].length + 1, endLineNumber: lineNumber, endColumn: lineContent.length + 1 }; + if (range.endColumn > range.startColumn) { + const sectionHeader = { + range, + ...getHeaderText(lineContent.substring(match[0].length)), + shouldBeInComments: false + }; + if (sectionHeader.text || sectionHeader.hasSeparatorLine) { + regionHeaders.push(sectionHeader); + } + } + } + } + return regionHeaders; +} + +function collectMarkHeaders(model: ISectionHeaderFinderTarget): SectionHeader[] { + const markHeaders: SectionHeader[] = []; + const endLineNumber = model.getLineCount(); + for (let lineNumber = 1; lineNumber <= endLineNumber; lineNumber++) { + const lineContent = model.getLineContent(lineNumber); + addMarkHeaderIfFound(lineContent, lineNumber, markHeaders); + } + return markHeaders; +} + +function addMarkHeaderIfFound(lineContent: string, lineNumber: number, sectionHeaders: SectionHeader[]) { + markRegex.lastIndex = 0; + const match = markRegex.exec(lineContent); + if (match) { + const column = match.indices![1][0] + 1; + const endColumn = match.indices![1][1] + 1; + const range = { startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: endColumn }; + if (range.endColumn > range.startColumn) { + const sectionHeader = { + range, + ...getHeaderText(match[1]), + shouldBeInComments: true + }; + if (sectionHeader.text || sectionHeader.hasSeparatorLine) { + sectionHeaders.push(sectionHeader); + } + } + } +} + +function getHeaderText(text: string): { text: string; hasSeparatorLine: boolean } { + text = text.trim(); + const hasSeparatorLine = text.startsWith('-'); + text = text.replace(trimDashesRegex, ''); + return { text, hasSeparatorLine }; +} diff --git a/code/src/vs/editor/common/standalone/standaloneEnums.ts b/code/src/vs/editor/common/standalone/standaloneEnums.ts index d2df0918567..3c960990910 100644 --- a/code/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/code/src/vs/editor/common/standalone/standaloneEnums.ts @@ -303,25 +303,26 @@ export enum EditorOption { useShadowDOM = 127, useTabStops = 128, wordBreak = 129, - wordSeparators = 130, - wordWrap = 131, - wordWrapBreakAfterCharacters = 132, - wordWrapBreakBeforeCharacters = 133, - wordWrapColumn = 134, - wordWrapOverride1 = 135, - wordWrapOverride2 = 136, - wrappingIndent = 137, - wrappingStrategy = 138, - showDeprecated = 139, - inlayHints = 140, - editorClassName = 141, - pixelRatio = 142, - tabFocusMode = 143, - layoutInfo = 144, - wrappingInfo = 145, - defaultColorDecorators = 146, - colorDecoratorsActivatedOn = 147, - inlineCompletionsAccessibilityVerbose = 148 + wordSegmenterLocales = 130, + wordSeparators = 131, + wordWrap = 132, + wordWrapBreakAfterCharacters = 133, + wordWrapBreakBeforeCharacters = 134, + wordWrapColumn = 135, + wordWrapOverride1 = 136, + wordWrapOverride2 = 137, + wrappingIndent = 138, + wrappingStrategy = 139, + showDeprecated = 140, + inlayHints = 141, + editorClassName = 142, + pixelRatio = 143, + tabFocusMode = 144, + layoutInfo = 145, + wrappingInfo = 146, + defaultColorDecorators = 147, + colorDecoratorsActivatedOn = 148, + inlineCompletionsAccessibilityVerbose = 149 } /** @@ -646,6 +647,14 @@ export enum MinimapPosition { Gutter = 2 } +/** + * Section header style. + */ +export enum MinimapSectionHeaderStyle { + Normal = 1, + Underlined = 2 +} + /** * Type of hit element with the mouse in the editor. */ @@ -740,6 +749,15 @@ export enum OverviewRulerLane { Full = 7 } +/** + * How a partial acceptance was triggered. + */ +export enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2 +} + export enum PositionAffinity { /** * Prefers the left most position. diff --git a/code/src/vs/editor/common/tokenizationTextModelPart.ts b/code/src/vs/editor/common/tokenizationTextModelPart.ts index 8884b008d98..07eb06f9fdc 100644 --- a/code/src/vs/editor/common/tokenizationTextModelPart.ts +++ b/code/src/vs/editor/common/tokenizationTextModelPart.ts @@ -56,6 +56,12 @@ export interface ITokenizationTextModelPart { */ tokenizeIfCheap(lineNumber: number): void; + /** + * Check if tokenization information is accurate for `lineNumber`. + * @internal + */ + hasAccurateTokensForLine(lineNumber: number): boolean; + /** * Check if calling `forceTokenization` for this `lineNumber` will be cheap (time-wise). * This is based on a heuristic. diff --git a/code/src/vs/editor/common/viewLayout/linesLayout.ts b/code/src/vs/editor/common/viewLayout/linesLayout.ts index 7bb55aeef6e..71bf9d5b956 100644 --- a/code/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/code/src/vs/editor/common/viewLayout/linesLayout.ts @@ -727,7 +727,8 @@ export class LinesLayout { relativeVerticalOffset: linesOffsets, centeredLineNumber: centeredLineNumber, completelyVisibleStartLineNumber: completelyVisibleStartLineNumber, - completelyVisibleEndLineNumber: completelyVisibleEndLineNumber + completelyVisibleEndLineNumber: completelyVisibleEndLineNumber, + lineHeight: this._lineHeight, }; } diff --git a/code/src/vs/editor/common/viewLayout/viewLinesViewportData.ts b/code/src/vs/editor/common/viewLayout/viewLinesViewportData.ts index 8ddcfddb99d..6e072c52648 100644 --- a/code/src/vs/editor/common/viewLayout/viewLinesViewportData.ts +++ b/code/src/vs/editor/common/viewLayout/viewLinesViewportData.ts @@ -46,6 +46,8 @@ export class ViewportData { private readonly _model: IViewModel; + public readonly lineHeight: number; + constructor( selections: Selection[], partialData: IPartialViewLinesViewportData, @@ -57,6 +59,7 @@ export class ViewportData { this.endLineNumber = partialData.endLineNumber | 0; this.relativeVerticalOffset = partialData.relativeVerticalOffset; this.bigNumbersDelta = partialData.bigNumbersDelta | 0; + this.lineHeight = partialData.lineHeight | 0; this.whitespaceViewportData = whitespaceViewportData; this._model = model; diff --git a/code/src/vs/editor/common/viewModel.ts b/code/src/vs/editor/common/viewModel.ts index 4f92417e89b..29a01bcf904 100644 --- a/code/src/vs/editor/common/viewModel.ts +++ b/code/src/vs/editor/common/viewModel.ts @@ -181,6 +181,11 @@ export interface IPartialViewLinesViewportData { * The last completely visible line number. */ readonly completelyVisibleEndLineNumber: number; + + /** + * The height of a line. + */ + readonly lineHeight: number; } export interface IViewWhitespaceViewportData { diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts index fb6ce190c81..c665dd778fe 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -25,6 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource, filtersAction, mayIncludeActionsOfKind } from '../common/types'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; export const codeActionCommandId = 'editor.action.codeAction'; export const quickFixCommandId = 'editor.action.quickFix'; @@ -79,7 +80,7 @@ class ManagedCodeActionSet extends Disposable implements CodeActionSet { } public get hasAutoFix() { - return this.validActions.some(({ action: fix }) => !!fix.kind && CodeActionKind.QuickFix.contains(new CodeActionKind(fix.kind)) && !!fix.isPreferred); + return this.validActions.some(({ action: fix }) => !!fix.kind && CodeActionKind.QuickFix.contains(new HierarchicalKind(fix.kind)) && !!fix.isPreferred); } public get hasAIFix() { @@ -178,7 +179,7 @@ function getCodeActionProviders( // We don't know what type of actions this provider will return. return true; } - return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new CodeActionKind(kind))); + return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new HierarchicalKind(kind))); }); } @@ -200,16 +201,16 @@ function* getAdditionalDocumentationForShowingActions( function getDocumentationFromProvider( provider: languages.CodeActionProvider, providedCodeActions: readonly languages.CodeAction[], - only?: CodeActionKind + only?: HierarchicalKind ): languages.Command | undefined { if (!provider.documentation) { return undefined; } - const documentation = provider.documentation.map(entry => ({ kind: new CodeActionKind(entry.kind), command: entry.command })); + const documentation = provider.documentation.map(entry => ({ kind: new HierarchicalKind(entry.kind), command: entry.command })); if (only) { - let currentBest: { readonly kind: CodeActionKind; readonly command: languages.Command } | undefined; + let currentBest: { readonly kind: HierarchicalKind; readonly command: languages.Command } | undefined; for (const entry of documentation) { if (entry.kind.contains(only)) { if (!currentBest) { @@ -234,7 +235,7 @@ function getDocumentationFromProvider( } for (const entry of documentation) { - if (entry.kind.contains(new CodeActionKind(action.kind))) { + if (entry.kind.contains(new HierarchicalKind(action.kind))) { return entry.command; } } @@ -347,7 +348,7 @@ CommandsRegistry.registerCommand('_executeCodeActionProvider', async function (a throw illegalArgument(); } - const include = typeof kind === 'string' ? new CodeActionKind(kind) : undefined; + const include = typeof kind === 'string' ? new HierarchicalKind(kind) : undefined; const codeActionSet = await getCodeActions( codeActionProvider, model, diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/code/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index 923c9181b7c..ae15cf9bae0 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -17,7 +18,7 @@ import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionFilter, CodeActio import { CodeActionController } from './codeActionController'; import { SUPPORTED_CODE_ACTIONS } from './codeActionModel'; -function contextKeyForSupportedActions(kind: CodeActionKind) { +function contextKeyForSupportedActions(kind: HierarchicalKind) { return ContextKeyExpr.regex( SUPPORTED_CODE_ACTIONS.keys()[0], new RegExp('(\\s|^)' + escapeRegExpCharacters(kind.value) + '\\b')); @@ -99,7 +100,7 @@ export class CodeActionCommand extends EditorCommand { public runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, userArgs: any) { const args = CodeActionCommandArgs.fromUser(userArgs, { - kind: CodeActionKind.Empty, + kind: HierarchicalKind.Empty, apply: CodeActionAutoApply.IfSingle, }); return triggerCodeActionsForEditorSelection(editor, @@ -164,7 +165,7 @@ export class RefactorAction extends EditorAction { ? nls.localize('editor.action.refactor.noneMessage.preferred', "No preferred refactorings available") : nls.localize('editor.action.refactor.noneMessage', "No refactorings available"), { - include: CodeActionKind.Refactor.contains(args.kind) ? args.kind : CodeActionKind.None, + include: CodeActionKind.Refactor.contains(args.kind) ? args.kind : HierarchicalKind.None, onlyIncludePreferredActions: args.preferred }, args.apply, CodeActionTriggerSource.Refactor); @@ -207,7 +208,7 @@ export class SourceAction extends EditorAction { ? nls.localize('editor.action.source.noneMessage.preferred', "No preferred source actions available") : nls.localize('editor.action.source.noneMessage', "No source actions available"), { - include: CodeActionKind.Source.contains(args.kind) ? args.kind : CodeActionKind.None, + include: CodeActionKind.Source.contains(args.kind) ? args.kind : HierarchicalKind.None, includeSourceActions: true, onlyIncludePreferredActions: args.preferred, }, diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts b/code/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts index 373d3a5a7c6..088fcfc9558 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { Lazy } from 'vs/base/common/lazy'; import { CodeAction } from 'vs/editor/common/languages'; @@ -11,7 +12,7 @@ import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/e import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; interface ResolveCodeActionKeybinding { - readonly kind: CodeActionKind; + readonly kind: HierarchicalKind; readonly preferred: boolean; readonly resolvedKeybinding: ResolvedKeybinding; } @@ -46,7 +47,7 @@ export class CodeActionKeybindingResolver { return { resolvedKeybinding: item.resolvedKeybinding!, ...CodeActionCommandArgs.fromUser(commandArgs, { - kind: CodeActionKind.None, + kind: HierarchicalKind.None, apply: CodeActionAutoApply.Never }) }; @@ -68,7 +69,7 @@ export class CodeActionKeybindingResolver { if (!action.kind) { return undefined; } - const kind = new CodeActionKind(action.kind); + const kind = new HierarchicalKind(action.kind); return candidates .filter(candidate => candidate.kind.contains(kind)) diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts b/code/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts index b8ebc3074a9..8763487cb1d 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts @@ -12,14 +12,15 @@ import { CodeActionItem, CodeActionKind } from 'vs/editor/contrib/codeAction/com import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors import { localize } from 'vs/nls'; import { ActionListItemKind, IActionListItem } from 'vs/platform/actionWidget/browser/actionList'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; interface ActionGroup { - readonly kind: CodeActionKind; + readonly kind: HierarchicalKind; readonly title: string; readonly icon?: ThemeIcon; } -const uncategorizedCodeActionGroup = Object.freeze({ kind: CodeActionKind.Empty, title: localize('codeAction.widget.id.more', 'More Actions...') }); +const uncategorizedCodeActionGroup = Object.freeze({ kind: HierarchicalKind.Empty, title: localize('codeAction.widget.id.more', 'More Actions...') }); const codeActionGroups = Object.freeze([ { kind: CodeActionKind.QuickFix, title: localize('codeAction.widget.id.quickfix', 'Quick Fix') }, @@ -54,7 +55,7 @@ export function toMenuItems( const menuEntries = codeActionGroups.map(group => ({ group, actions: [] as CodeActionItem[] })); for (const action of inputCodeActions) { - const kind = action.action.kind ? new CodeActionKind(action.action.kind) : CodeActionKind.None; + const kind = action.action.kind ? new HierarchicalKind(action.action.kind) : HierarchicalKind.None; for (const menuEntry of menuEntries) { if (menuEntry.group.kind.contains(kind)) { menuEntry.actions.push(action); diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index 547b6c43108..9c7e0c73b76 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts @@ -15,12 +15,13 @@ import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { CodeActionProvider, CodeActionTriggerType } from 'vs/editor/common/languages'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEditorProgressService, Progress } from 'vs/platform/progress/common/progress'; import { CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types'; import { getCodeActions } from './codeAction'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; export const SUPPORTED_CODE_ACTIONS = new RawContextKey('supportedCodeAction', ''); @@ -235,7 +236,7 @@ export class CodeActionModel extends Disposable { } // Search for quickfixes in the curret code action set. - const foundQuickfix = codeActionSet.validActions?.some(action => action.action.kind ? CodeActionKind.QuickFix.contains(new CodeActionKind(action.action.kind)) : false); + const foundQuickfix = codeActionSet.validActions?.some(action => action.action.kind ? CodeActionKind.QuickFix.contains(new HierarchicalKind(action.action.kind)) : false); const allMarkers = this._markerService.read({ resource: model.uri }); if (foundQuickfix) { for (const action of codeActionSet.validActions) { @@ -320,7 +321,20 @@ export class CodeActionModel extends Disposable { if (trigger.trigger.type === CodeActionTriggerType.Invoke) { this._progressService?.showWhile(actions, 250); } - this.setState(new CodeActionsState.Triggered(trigger.trigger, startPosition, actions)); + const newState = new CodeActionsState.Triggered(trigger.trigger, startPosition, actions); + let isManualToAutoTransition = false; + if (this._state.type === CodeActionsState.Type.Triggered) { + // Check if the current state is manual and the new state is automatic + isManualToAutoTransition = this._state.trigger.type === CodeActionTriggerType.Invoke && + newState.type === CodeActionsState.Type.Triggered && + newState.trigger.type === CodeActionTriggerType.Auto && + this._state.position !== newState.position; + } + + // Do not trigger state if current state is manual and incoming state is automatic + if (!isManualToAutoTransition) { + this.setState(newState); + } }, undefined); this._codeActionOracle.value.trigger({ type: CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default }); } else { diff --git a/code/src/vs/editor/contrib/codeAction/common/types.ts b/code/src/vs/editor/contrib/codeAction/common/types.ts index 19a690e23dc..febdfda4d39 100644 --- a/code/src/vs/editor/contrib/codeAction/common/types.ts +++ b/code/src/vs/editor/contrib/codeAction/common/types.ts @@ -5,47 +5,27 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Position } from 'vs/editor/common/core/position'; import * as languages from 'vs/editor/common/languages'; import { ActionSet } from 'vs/platform/actionWidget/common/actionWidget'; -export class CodeActionKind { - private static readonly sep = '.'; - - public static readonly None = new CodeActionKind('@@none@@'); // Special code action that contains nothing - public static readonly Empty = new CodeActionKind(''); - public static readonly QuickFix = new CodeActionKind('quickfix'); - public static readonly Refactor = new CodeActionKind('refactor'); - public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); - public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); - public static readonly RefactorMove = CodeActionKind.Refactor.append('move'); - public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); - public static readonly Notebook = new CodeActionKind('notebook'); - public static readonly Source = new CodeActionKind('source'); - public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); - public static readonly SourceFixAll = CodeActionKind.Source.append('fixAll'); - public static readonly SurroundWith = CodeActionKind.Refactor.append('surround'); +export const CodeActionKind = new class { + public readonly QuickFix = new HierarchicalKind('quickfix'); - constructor( - public readonly value: string - ) { } + public readonly Refactor = new HierarchicalKind('refactor'); + public readonly RefactorExtract = this.Refactor.append('extract'); + public readonly RefactorInline = this.Refactor.append('inline'); + public readonly RefactorMove = this.Refactor.append('move'); + public readonly RefactorRewrite = this.Refactor.append('rewrite'); - public equals(other: CodeActionKind): boolean { - return this.value === other.value; - } - - public contains(other: CodeActionKind): boolean { - return this.equals(other) || this.value === '' || other.value.startsWith(this.value + CodeActionKind.sep); - } + public readonly Notebook = new HierarchicalKind('notebook'); - public intersects(other: CodeActionKind): boolean { - return this.contains(other) || other.contains(this); - } - - public append(part: string): CodeActionKind { - return new CodeActionKind(this.value + CodeActionKind.sep + part); - } -} + public readonly Source = new HierarchicalKind('source'); + public readonly SourceOrganizeImports = this.Source.append('organizeImports'); + public readonly SourceFixAll = this.Source.append('fixAll'); + public readonly SurroundWith = this.Refactor.append('surround'); +}; export const enum CodeActionAutoApply { IfSingle = 'ifSingle', @@ -69,13 +49,13 @@ export enum CodeActionTriggerSource { } export interface CodeActionFilter { - readonly include?: CodeActionKind; - readonly excludes?: readonly CodeActionKind[]; + readonly include?: HierarchicalKind; + readonly excludes?: readonly HierarchicalKind[]; readonly includeSourceActions?: boolean; readonly onlyIncludePreferredActions?: boolean; } -export function mayIncludeActionsOfKind(filter: CodeActionFilter, providedKind: CodeActionKind): boolean { +export function mayIncludeActionsOfKind(filter: CodeActionFilter, providedKind: HierarchicalKind): boolean { // A provided kind may be a subset or superset of our filtered kind. if (filter.include && !filter.include.intersects(providedKind)) { return false; @@ -96,7 +76,7 @@ export function mayIncludeActionsOfKind(filter: CodeActionFilter, providedKind: } export function filtersAction(filter: CodeActionFilter, action: languages.CodeAction): boolean { - const actionKind = action.kind ? new CodeActionKind(action.kind) : undefined; + const actionKind = action.kind ? new HierarchicalKind(action.kind) : undefined; // Filter out actions by kind if (filter.include) { @@ -127,7 +107,7 @@ export function filtersAction(filter: CodeActionFilter, action: languages.CodeAc return true; } -function excludesAction(providedKind: CodeActionKind, exclude: CodeActionKind, include: CodeActionKind | undefined): boolean { +function excludesAction(providedKind: HierarchicalKind, exclude: HierarchicalKind, include: HierarchicalKind | undefined): boolean { if (!exclude.contains(providedKind)) { return false; } @@ -150,7 +130,7 @@ export interface CodeActionTrigger { } export class CodeActionCommandArgs { - public static fromUser(arg: any, defaults: { kind: CodeActionKind; apply: CodeActionAutoApply }): CodeActionCommandArgs { + public static fromUser(arg: any, defaults: { kind: HierarchicalKind; apply: CodeActionAutoApply }): CodeActionCommandArgs { if (!arg || typeof arg !== 'object') { return new CodeActionCommandArgs(defaults.kind, defaults.apply, false); } @@ -169,9 +149,9 @@ export class CodeActionCommandArgs { } } - private static getKindFromUser(arg: any, defaultKind: CodeActionKind) { + private static getKindFromUser(arg: any, defaultKind: HierarchicalKind) { return typeof arg.kind === 'string' - ? new CodeActionKind(arg.kind) + ? new HierarchicalKind(arg.kind) : defaultKind; } @@ -182,7 +162,7 @@ export class CodeActionCommandArgs { } private constructor( - public readonly kind: CodeActionKind, + public readonly kind: HierarchicalKind, public readonly apply: CodeActionAutoApply, public readonly preferred: boolean, ) { } diff --git a/code/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts b/code/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts index ac919bb6192..c1783a39f11 100644 --- a/code/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts +++ b/code/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -148,20 +149,20 @@ suite('CodeAction', () => { disposables.add(registry.register('fooLang', provider)); { - const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None)); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new HierarchicalKind('a') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 2); assert.strictEqual(actions[0].action.title, 'a'); assert.strictEqual(actions[1].action.title, 'a.b'); } { - const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a.b') } }, Progress.None, CancellationToken.None)); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new HierarchicalKind('a.b') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'a.b'); } { - const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a.b.c') } }, Progress.None, CancellationToken.None)); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new HierarchicalKind('a.b.c') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 0); } }); @@ -180,7 +181,7 @@ suite('CodeAction', () => { disposables.add(registry.register('fooLang', provider)); - const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None)); + const { validActions: actions } = disposables.add(await getCodeActions(registry, model, new Range(1, 1, 2, 1), { type: languages.CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default, filter: { include: new HierarchicalKind('a') } }, Progress.None, CancellationToken.None)); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].action.title, 'a'); }); diff --git a/code/src/vs/editor/contrib/comment/browser/comment.ts b/code/src/vs/editor/contrib/comment/browser/comment.ts index 538d6a3ca6c..2a2a4c28360 100644 --- a/code/src/vs/editor/contrib/comment/browser/comment.ts +++ b/code/src/vs/editor/contrib/comment/browser/comment.ts @@ -63,7 +63,7 @@ abstract class CommentLineAction extends EditorAction { commands.push(new LineCommentCommand( languageConfigurationService, selection.selection, - modelOptions.tabSize, + modelOptions.indentSize, this._type, commentsOptions.insertSpace, commentsOptions.ignoreEmptyLines, diff --git a/code/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts b/code/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts index ac498e8a7e4..a5e19fd79e1 100644 --- a/code/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts +++ b/code/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts @@ -50,7 +50,7 @@ export const enum Type { export class LineCommentCommand implements ICommand { private readonly _selection: Selection; - private readonly _tabSize: number; + private readonly _indentSize: number; private readonly _type: Type; private readonly _insertSpace: boolean; private readonly _ignoreEmptyLines: boolean; @@ -62,14 +62,14 @@ export class LineCommentCommand implements ICommand { constructor( private readonly languageConfigurationService: ILanguageConfigurationService, selection: Selection, - tabSize: number, + indentSize: number, type: Type, insertSpace: boolean, ignoreEmptyLines: boolean, ignoreFirstLine?: boolean, ) { this._selection = selection; - this._tabSize = tabSize; + this._indentSize = indentSize; this._type = type; this._insertSpace = insertSpace; this._selectionId = null; @@ -209,7 +209,7 @@ export class LineCommentCommand implements ICommand { if (data.shouldRemoveComments) { ops = LineCommentCommand._createRemoveLineCommentsOperations(data.lines, s.startLineNumber); } else { - LineCommentCommand._normalizeInsertionPoint(model, data.lines, s.startLineNumber, this._tabSize); + LineCommentCommand._normalizeInsertionPoint(model, data.lines, s.startLineNumber, this._indentSize); ops = this._createAddLineCommentsOperations(data.lines, s.startLineNumber); } @@ -420,9 +420,9 @@ export class LineCommentCommand implements ICommand { return res; } - private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number { + private static nextVisibleColumn(currentVisibleColumn: number, indentSize: number, isTab: boolean, columnSize: number): number { if (isTab) { - return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize)); + return currentVisibleColumn + (indentSize - (currentVisibleColumn % indentSize)); } return currentVisibleColumn + columnSize; } @@ -430,7 +430,7 @@ export class LineCommentCommand implements ICommand { /** * Adjust insertion points to have them vertically aligned in the add line comment case */ - public static _normalizeInsertionPoint(model: ISimpleModel, lines: IInsertionPoint[], startLineNumber: number, tabSize: number): void { + public static _normalizeInsertionPoint(model: ISimpleModel, lines: IInsertionPoint[], startLineNumber: number, indentSize: number): void { let minVisibleColumn = Constants.MAX_SAFE_SMALL_INTEGER; let j: number; let lenJ: number; @@ -444,7 +444,7 @@ export class LineCommentCommand implements ICommand { let currentVisibleColumn = 0; for (let j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) { - currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, tabSize, lineContent.charCodeAt(j) === CharCode.Tab, 1); + currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, indentSize, lineContent.charCodeAt(j) === CharCode.Tab, 1); } if (currentVisibleColumn < minVisibleColumn) { @@ -452,7 +452,7 @@ export class LineCommentCommand implements ICommand { } } - minVisibleColumn = Math.floor(minVisibleColumn / tabSize) * tabSize; + minVisibleColumn = Math.floor(minVisibleColumn / indentSize) * indentSize; for (let i = 0, len = lines.length; i < len; i++) { if (lines[i].ignore) { @@ -463,7 +463,7 @@ export class LineCommentCommand implements ICommand { let currentVisibleColumn = 0; for (j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) { - currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, tabSize, lineContent.charCodeAt(j) === CharCode.Tab, 1); + currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, indentSize, lineContent.charCodeAt(j) === CharCode.Tab, 1); } if (currentVisibleColumn > minVisibleColumn) { diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts b/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts index fbdd84d88d4..20ed163b4b2 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; +import { IJSONSchema, SchemaToType } from 'vs/base/common/jsonSchema'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorFeature } from 'vs/editor/common/editorFeatures'; import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController'; -import { DefaultPasteProvidersFeature } from 'vs/editor/contrib/dropOrPasteInto/browser/defaultProviders'; +import { DefaultPasteProvidersFeature, DefaultTextPasteOrDropEditProvider } from 'vs/editor/contrib/dropOrPasteInto/browser/defaultProviders'; import * as nls from 'vs/nls'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; registerEditorContribution(CopyPasteController.ID, CopyPasteController, EditorContributionInstantiation.Eager); // eager because it listens to events on the container dom node of the editor - registerEditorFeature(DefaultPasteProvidersFeature); registerEditorCommand(new class extends EditorCommand { @@ -29,12 +30,40 @@ registerEditorCommand(new class extends EditorCommand { }); } - public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor, _args: any) { + public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor) { return CopyPasteController.get(editor)?.changePasteType(); } }); -registerEditorAction(class extends EditorAction { +registerEditorCommand(new class extends EditorCommand { + constructor() { + super({ + id: 'editor.hidePasteWidget', + precondition: pasteWidgetVisibleCtx, + kbOpts: { + weight: KeybindingWeight.EditorContrib, + primary: KeyCode.Escape, + } + }); + } + + public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor) { + CopyPasteController.get(editor)?.clearWidgets(); + } +}); + + +registerEditorAction(class PasteAsAction extends EditorAction { + private static readonly argsSchema = { + type: 'object', + properties: { + kind: { + type: 'string', + description: nls.localize('pasteAs.kind', "The kind of the paste edit to try applying. If not provided or there are multiple edits for this kind, the editor will show a picker."), + } + }, + } as const satisfies IJSONSchema; + constructor() { super({ id: 'editor.action.pasteAs', @@ -45,23 +74,20 @@ registerEditorAction(class extends EditorAction { description: 'Paste as', args: [{ name: 'args', - schema: { - type: 'object', - properties: { - 'id': { - type: 'string', - description: nls.localize('pasteAs.id', "The id of the paste edit to try applying. If not provided, the editor will show a picker."), - } - }, - } + schema: PasteAsAction.argsSchema }] } }); } - public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any) { - const id = typeof args?.id === 'string' ? args.id : undefined; - return CopyPasteController.get(editor)?.pasteAs(id); + public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args?: SchemaToType) { + let kind = typeof args?.kind === 'string' ? args.kind : undefined; + if (!kind && args) { + // Support old id property + // TODO: remove this in the future + kind = typeof (args as any).id === 'string' ? (args as any).id : undefined; + } + return CopyPasteController.get(editor)?.pasteAs(kind ? new HierarchicalKind(kind) : undefined); } }); @@ -75,7 +101,7 @@ registerEditorAction(class extends EditorAction { }); } - public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any) { - return CopyPasteController.get(editor)?.pasteAs('text'); + public override run(_accessor: ServicesAccessor, editor: ICodeEditor) { + return CopyPasteController.get(editor)?.pasteAs({ providerId: DefaultTextPasteOrDropEditProvider.id }); } }); diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index adc0684cfca..87cddb00a8d 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -8,24 +8,27 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { UriList, VSDataTransfer, createStringDataTransferItem, matchesMimeType } from 'vs/base/common/dataTransfer'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import * as platform from 'vs/base/common/platform'; import { generateUuid } from 'vs/base/common/uuid'; import { ClipboardEventUtils } from 'vs/editor/browser/controller/textAreaInput'; import { toExternalVSDataTransfer, toVSDataTransfer } from 'vs/editor/browser/dnd'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, PastePayload } from 'vs/editor/browser/editorBrowser'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { Handler, IEditorContribution, PastePayload } from 'vs/editor/common/editorCommon'; -import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages'; +import { Handler, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { DefaultTextPasteOrDropEditProvider } from 'vs/editor/contrib/dropOrPasteInto/browser/defaultProviders'; import { createCombinedWorkspaceEdit, sortEditsByYieldTo } from 'vs/editor/contrib/dropOrPasteInto/browser/edit'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState'; import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -33,7 +36,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { PostEditWidgetManager } from './postEditWidget'; -import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; export const changePasteTypeCommandId = 'editor.changePasteType'; @@ -48,6 +50,14 @@ interface CopyMetadata { readonly defaultPastePayload: Omit; } +type PasteEditWithProvider = DocumentPasteEdit & { + provider: DocumentPasteEditProvider; +}; + +type PastePreference = + | HierarchicalKind + | { providerId: string }; + export class CopyPasteController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.copyPasteActionController'; @@ -71,10 +81,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi private readonly _editor: ICodeEditor; private _currentPasteOperation?: CancelablePromise; - private _pasteAsActionContext?: { readonly preferredId: string | undefined }; + private _pasteAsActionContext?: { readonly preferred?: PastePreference }; private readonly _pasteProgressManager: InlineProgressManager; - private readonly _postPasteWidgetManager: PostEditWidgetManager; + private readonly _postPasteWidgetManager: PostEditWidgetManager; constructor( editor: ICodeEditor, @@ -103,10 +113,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._postPasteWidgetManager.tryShowSelector(); } - public pasteAs(preferredId?: string) { + public pasteAs(preferred?: PastePreference) { this._editor.focus(); try { - this._pasteAsActionContext = { preferredId }; + this._pasteAsActionContext = { preferred }; getActiveDocument().execCommand('paste'); } finally { this._pasteAsActionContext = undefined; @@ -253,17 +263,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi const allProviders = this._languageFeaturesService.documentPasteEditProvider .ordered(model) .filter(provider => { - if (this._pasteAsActionContext?.preferredId) { - if (this._pasteAsActionContext.preferredId !== provider.id) { + // Filter out providers that don't match the requested paste types + const preference = this._pasteAsActionContext?.preferred; + if (preference) { + if (provider.providedPasteEditKinds && !this.providerMatchesPreference(provider, preference)) { return false; } } + // And providers that don't handle any of mime types in the clipboard return provider.pasteMimeTypes?.some(type => matchesMimeType(type, allPotentialMimeTypes)); }); if (!allProviders.length) { - if (this._pasteAsActionContext?.preferredId) { - this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext?.preferredId); + if (this._pasteAsActionContext?.preferred) { + this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext.preferred); } return; } @@ -275,17 +288,17 @@ export class CopyPasteController extends Disposable implements IEditorContributi e.stopImmediatePropagation(); if (this._pasteAsActionContext) { - this.showPasteAsPick(this._pasteAsActionContext.preferredId, allProviders, selections, dataTransfer, metadata, { trigger: 'explicit', only: this._pasteAsActionContext.preferredId }); + this.showPasteAsPick(this._pasteAsActionContext.preferred, allProviders, selections, dataTransfer, metadata); } else { - this.doPasteInline(allProviders, selections, dataTransfer, metadata, { trigger: 'implicit' }); + this.doPasteInline(allProviders, selections, dataTransfer, metadata, e); } } - private showPasteAsNoEditMessage(selections: readonly Selection[], editId: string) { - MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", editId), selections[0].getStartPosition()); + private showPasteAsNoEditMessage(selections: readonly Selection[], preference: PastePreference) { + MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", preference instanceof HierarchicalKind ? preference.value : preference.providerId), selections[0].getStartPosition()); } - private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, context: DocumentPasteContext): void { + private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void { const p = createCancelablePromise(async (token) => { const editor = this._editor; if (!editor.hasModel()) { @@ -300,32 +313,38 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - // Filter out any providers the don't match the full data transfer we will send them. - const supportedProviders = allProviders.filter(provider => isSupportedPasteProvider(provider, dataTransfer)); + const supportedProviders = allProviders.filter(provider => this.isSupportedPasteProvider(provider, dataTransfer)); if (!supportedProviders.length - || (supportedProviders.length === 1 && supportedProviders[0].id === 'text') // Only our default text provider is active + || (supportedProviders.length === 1 && supportedProviders[0] instanceof DefaultTextPasteOrDropEditProvider) // Only our default text provider is active ) { - await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token); - return; + return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); } + const context: DocumentPasteContext = { + triggerKind: DocumentPasteTriggerKind.Automatic, + }; const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); if (tokenSource.token.isCancellationRequested) { return; } - // If the only edit returned is a text edit, use the default paste handler - if (providerEdits.length === 1 && providerEdits[0].providerId === 'text') { - await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token); - return; + // If the only edit returned is our default text edit, use the default paste handler + if (providerEdits.length === 1 && providerEdits[0].provider instanceof DefaultTextPasteOrDropEditProvider) { + return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); } if (providerEdits.length) { const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste'; - return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: providerEdits }, canShowWidget, tokenSource.token); + return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: providerEdits }, canShowWidget, async (edit, token) => { + const resolved = await edit.provider.resolveDocumentPasteEdit?.(edit, token); + if (resolved) { + edit.additionalEdit = resolved.additionalEdit; + } + return edit; + }, tokenSource.token); } - await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token); + await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); } finally { tokenSource.dispose(); if (this._currentPasteOperation === p) { @@ -338,7 +357,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._currentPasteOperation = p; } - private showPasteAsPick(preferredId: string | undefined, allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, context: DocumentPasteContext): void { + private showPasteAsPick(preference: PastePreference | undefined, allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void { const p = createCancelablePromise(async (token) => { const editor = this._editor; if (!editor.hasModel()) { @@ -354,17 +373,32 @@ export class CopyPasteController extends Disposable implements IEditorContributi } // Filter out any providers the don't match the full data transfer we will send them. - let supportedProviders = allProviders.filter(provider => isSupportedPasteProvider(provider, dataTransfer)); - if (preferredId) { + let supportedProviders = allProviders.filter(provider => this.isSupportedPasteProvider(provider, dataTransfer, preference)); + if (preference) { // We are looking for a specific edit - supportedProviders = supportedProviders.filter(edit => edit.id === preferredId); + supportedProviders = supportedProviders.filter(provider => this.providerMatchesPreference(provider, preference)); } - const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); + const context: DocumentPasteContext = { + triggerKind: DocumentPasteTriggerKind.PasteAs, + only: preference && preference instanceof HierarchicalKind ? preference : undefined, + }; + let providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); if (tokenSource.token.isCancellationRequested) { return; } + // Filter out any edits that don't match the requested kind + if (preference) { + providerEdits = providerEdits.filter(edit => { + if (preference instanceof HierarchicalKind) { + return preference.contains(edit.kind); + } else { + return preference.providerId === edit.provider.id; + } + }); + } + if (!providerEdits.length) { if (context.only) { this.showPasteAsNoEditMessage(selections, context.only); @@ -373,14 +407,13 @@ export class CopyPasteController extends Disposable implements IEditorContributi } let pickedEdit: DocumentPasteEdit | undefined; - if (preferredId) { + if (preference) { pickedEdit = providerEdits.at(0); } else { const selected = await this._quickInputService.pick( providerEdits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ - label: edit.label, - description: edit.providerId, - detail: edit.detail, + label: edit.title, + description: edit.kind?.value, edit, })), { placeHolder: localize('pasteAsPickerPlaceholder', "Select Paste Action"), @@ -466,36 +499,34 @@ export class CopyPasteController extends Disposable implements IEditorContributi } } - private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise> { + private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { const results = await raceCancellation( Promise.all(providers.map(async provider => { try { - const edit = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token); - if (edit) { - return { ...edit, providerId: provider.id }; - } + const edits = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token); + // TODO: dispose of edits + return edits?.edits?.map(edit => ({ ...edit, provider })); } catch (err) { console.error(err); } return undefined; })), token); - const edits = coalesce(results ?? []); + const edits = coalesce(results ?? []).flat().filter(edit => { + return !context.only || context.only.contains(edit.kind); + }); return sortEditsByYieldTo(edits); } - private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken) { + private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent) { const textDataTransfer = dataTransfer.get(Mimes.text) ?? dataTransfer.get('text'); - if (!textDataTransfer) { - return; - } - - const text = await textDataTransfer.asString(); + const text = (await textDataTransfer?.asString()) ?? ''; if (token.isCancellationRequested) { return; } const payload: PastePayload = { + clipboardEvent, text, pasteOnNewLine: metadata?.defaultPastePayload.pasteOnNewLine ?? false, multicursorText: metadata?.defaultPastePayload.multicursorText ?? null, @@ -503,8 +534,28 @@ export class CopyPasteController extends Disposable implements IEditorContributi }; this._editor.trigger('keyboard', Handler.Paste, payload); } -} -function isSupportedPasteProvider(provider: DocumentPasteEditProvider, dataTransfer: VSDataTransfer): boolean { - return Boolean(provider.pasteMimeTypes?.some(type => dataTransfer.matches(type))); + /** + * Filter out providers if they: + * - Don't handle any of the data transfer types we have + * - Don't match the preferred paste kind + */ + private isSupportedPasteProvider(provider: DocumentPasteEditProvider, dataTransfer: VSDataTransfer, preference?: PastePreference): boolean { + if (!provider.pasteMimeTypes?.some(type => dataTransfer.matches(type))) { + return false; + } + + return !preference || this.providerMatchesPreference(provider, preference); + } + + private providerMatchesPreference(provider: DocumentPasteEditProvider, preference: PastePreference): boolean { + if (preference instanceof HierarchicalKind) { + if (!provider.providedPasteEditKinds) { + return true; + } + return provider.providedPasteEditKinds.some(providedKind => preference.contains(providedKind)); + } else { + return provider.id === preference.providerId; + } + } } diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/code/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 27812236c50..88ffc64d4bd 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -6,6 +6,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IReadonlyVSDataTransfer, UriList } from 'vs/base/common/dataTransfer'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { Schemas } from 'vs/base/common/network'; @@ -13,36 +14,46 @@ import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; -import { DocumentOnDropEdit, DocumentOnDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages'; +import { DocumentOnDropEdit, DocumentOnDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -const builtInLabel = localize('builtIn', 'Built-in'); abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, DocumentPasteEditProvider { - abstract readonly id: string; + abstract readonly kind: HierarchicalKind; abstract readonly dropMimeTypes: readonly string[] | undefined; abstract readonly pasteMimeTypes: readonly string[]; - async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? { insertText: edit.insertText, label: edit.label, detail: edit.detail, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo } : undefined; + if (!edit) { + return undefined; + } + + return { + dispose() { }, + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] + }; } - async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? { insertText: edit.insertText, label: edit.label, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo } : undefined; + return edit ? [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] : undefined; } protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise; } -class DefaultTextProvider extends SimplePasteAndDropProvider { +export class DefaultTextPasteOrDropEditProvider extends SimplePasteAndDropProvider { + + static readonly id = 'text'; + static readonly kind = new HierarchicalKind('text.plain'); - readonly id = 'text'; + readonly id = DefaultTextPasteOrDropEditProvider.id; + readonly kind = DefaultTextPasteOrDropEditProvider.kind; readonly dropMimeTypes = [Mimes.text]; readonly pasteMimeTypes = [Mimes.text]; @@ -61,16 +72,16 @@ class DefaultTextProvider extends SimplePasteAndDropProvider { const insertText = await textEntry.asString(); return { handledMimeType: Mimes.text, - label: localize('text.label', "Insert Plain Text"), - detail: builtInLabel, - insertText + title: localize('text.label', "Insert Plain Text"), + insertText, + kind: this.kind, }; } } class PathProvider extends SimplePasteAndDropProvider { - readonly id = 'uri'; + readonly kind = new HierarchicalKind('uri.absolute'); readonly dropMimeTypes = [Mimes.uriList]; readonly pasteMimeTypes = [Mimes.uriList]; @@ -108,15 +119,15 @@ class PathProvider extends SimplePasteAndDropProvider { return { handledMimeType: Mimes.uriList, insertText, - label, - detail: builtInLabel, + title: label, + kind: this.kind, }; } } class RelativePathProvider extends SimplePasteAndDropProvider { - readonly id = 'relativePath'; + readonly kind = new HierarchicalKind('uri.relative'); readonly dropMimeTypes = [Mimes.uriList]; readonly pasteMimeTypes = [Mimes.uriList]; @@ -144,24 +155,24 @@ class RelativePathProvider extends SimplePasteAndDropProvider { return { handledMimeType: Mimes.uriList, insertText: relativeUris.join(' '), - label: entries.length > 1 + title: entries.length > 1 ? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths") : localize('defaultDropProvider.uriList.relativePath', "Insert Relative Path"), - detail: builtInLabel, + kind: this.kind, }; } } class PasteHtmlProvider implements DocumentPasteEditProvider { - public readonly id = 'html'; + public readonly kind = new HierarchicalKind('html'); public readonly pasteMimeTypes = ['text/html']; private readonly _yieldTo = [{ mimeType: Mimes.text }]; - async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { - if (context.trigger !== 'explicit' && context.only !== this.id) { + async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + if (context.triggerKind !== DocumentPasteTriggerKind.PasteAs && !context.only?.contains(this.kind)) { return; } @@ -172,10 +183,13 @@ class PasteHtmlProvider implements DocumentPasteEditProvider { } return { - insertText: htmlText, - yieldTo: this._yieldTo, - label: localize('pasteHtmlLabel', 'Insert HTML'), - detail: builtInLabel, + dispose() { }, + edits: [{ + insertText: htmlText, + yieldTo: this._yieldTo, + title: localize('pasteHtmlLabel', 'Insert HTML'), + kind: this.kind, + }], }; } } @@ -205,7 +219,7 @@ export class DefaultDropProvidersFeature extends Disposable { ) { super(); - this._register(languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultTextProvider())); + this._register(languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultTextPasteOrDropEditProvider())); this._register(languageFeaturesService.documentOnDropEditProvider.register('*', new PathProvider())); this._register(languageFeaturesService.documentOnDropEditProvider.register('*', new RelativePathProvider(workspaceContextService))); } @@ -218,7 +232,7 @@ export class DefaultPasteProvidersFeature extends Disposable { ) { super(); - this._register(languageFeaturesService.documentPasteEditProvider.register('*', new DefaultTextProvider())); + this._register(languageFeaturesService.documentPasteEditProvider.register('*', new DefaultTextPasteOrDropEditProvider())); this._register(languageFeaturesService.documentPasteEditProvider.register('*', new PathProvider())); this._register(languageFeaturesService.documentPasteEditProvider.register('*', new RelativePathProvider(workspaceContextService))); this._register(languageFeaturesService.documentPasteEditProvider.register('*', new PasteHtmlProvider())); diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts b/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts index 4817431d189..52dc73b8ce2 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts @@ -16,6 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { DropIntoEditorController, changeDropTypeCommandId, defaultProviderConfig, dropWidgetVisibleCtx } from './dropIntoEditorController'; registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController, EditorContributionInstantiation.BeforeFirstInteraction); +registerEditorFeature(DefaultDropProvidersFeature); registerEditorCommand(new class extends EditorCommand { constructor() { @@ -34,7 +35,22 @@ registerEditorCommand(new class extends EditorCommand { } }); -registerEditorFeature(DefaultDropProvidersFeature); +registerEditorCommand(new class extends EditorCommand { + constructor() { + super({ + id: 'editor.hideDropWidget', + precondition: dropWidgetVisibleCtx, + kbOpts: { + weight: KeybindingWeight.EditorContrib, + primary: KeyCode.Escape, + } + }); + } + + public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor, _args: any) { + DropIntoEditorController.get(editor)?.clearWidgets(); + } +}); Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ ...editorConfigurationBaseNode, diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 48dae75565c..32e64cff650 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -6,6 +6,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { VSDataTransfer, matchesMimeType } from 'vs/base/common/dataTransfer'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -45,7 +46,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr private _currentOperation?: CancelablePromise; private readonly _dropProgressManager: InlineProgressManager; - private readonly _postDropWidgetManager: PostEditWidgetManager; + private readonly _postDropWidgetManager: PostEditWidgetManager; private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance(); @@ -115,7 +116,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr 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, allEdits: edits }, canShowWidget, token); + await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, async edit => edit, token); } } finally { tokenSource.dispose(); @@ -132,25 +133,24 @@ export class DropIntoEditorController extends Disposable implements IEditorContr 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); - if (edit) { - return { ...edit, providerId: provider.id }; - } + const edits = await provider.provideDocumentOnDropEdits(model, position, dataTransfer, tokenSource.token); + return edits?.map(edit => ({ ...edit, providerId: provider.id })); } catch (err) { console.error(err); } return undefined; })), tokenSource.token); - const edits = coalesce(results ?? []); + const edits = coalesce(results ?? []).flat(); 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)) { + for (const [configMime, desiredKindStr] of Object.entries(preferredProviders)) { + const desiredKind = new HierarchicalKind(desiredKindStr); const editIndex = edits.findIndex(edit => - desiredId === edit.providerId + desiredKind.value === edit.providerId && edit.handledMimeType && matchesMimeType(configMime, [edit.handledMimeType])); if (editIndex >= 0) { return editIndex; diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts b/code/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts index 55d8dff9e7b..81cc89436cf 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts @@ -5,21 +5,16 @@ import { URI } from 'vs/base/common/uri'; import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { DropYieldTo, WorkspaceEdit } from 'vs/editor/common/languages'; +import { DocumentOnDropEdit, DocumentPasteEdit, DropYieldTo, WorkspaceEdit } from 'vs/editor/common/languages'; import { Range } from 'vs/editor/common/core/range'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; - -export interface DropOrPasteEdit { - readonly label: string; - readonly insertText: string | { readonly snippet: string }; - readonly additionalEdit?: WorkspaceEdit; -} +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; /** * Given a {@link DropOrPasteEdit} and set of ranges, creates a {@link WorkspaceEdit} that applies the insert text from * the {@link DropOrPasteEdit} at each range plus any additional edits. */ -export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], edit: DropOrPasteEdit): WorkspaceEdit { +export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], edit: DocumentPasteEdit | DocumentOnDropEdit): WorkspaceEdit { // If the edit insert text is empty, skip applying at each range if (typeof edit.insertText === 'string' ? edit.insertText === '' : edit.insertText.snippet === '') { return { @@ -39,13 +34,15 @@ export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], } export function sortEditsByYieldTo(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); + if ('mimeType' in yTo) { + return yTo.mimeType === other.handledMimeType; + } + return !!other.kind && yTo.kind.contains(other.kind); } // Build list of nodes each node yields to @@ -84,7 +81,7 @@ export function sortEditsByYieldTo { readonly activeEditIndex: number; - readonly allEdits: ReadonlyArray<{ - readonly label: string; - readonly insertText: string | { readonly snippet: string }; - readonly additionalEdit?: WorkspaceEdit; - }>; + readonly allEdits: ReadonlyArray; } interface ShowCommand { @@ -36,7 +32,7 @@ interface ShowCommand { readonly label: string; } -class PostEditWidget extends Disposable implements IContentWidget { +class PostEditWidget extends Disposable implements IContentWidget { private static readonly baseId = 'editor.widget.postEditWidget'; readonly allowEditorOverflow = true; @@ -53,7 +49,7 @@ class PostEditWidget extends Disposable implements IContentWidget { visibleContext: RawContextKey, private readonly showCommand: ShowCommand, private readonly range: Range, - private readonly edits: EditSet, + private readonly edits: EditSet, private readonly onSelectNewEdit: (editIndex: number) => void, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService, @@ -123,7 +119,7 @@ class PostEditWidget extends Disposable implements IContentWidget { getActions: () => { return this.edits.allEdits.map((edit, i) => toAction({ id: '', - label: edit.label, + label: edit.title, checked: i === this.edits.activeEditIndex, run: () => { if (i !== this.edits.activeEditIndex) { @@ -136,9 +132,9 @@ class PostEditWidget extends Disposable implements IContentWidget { } } -export class PostEditWidgetManager extends Disposable { +export class PostEditWidgetManager extends Disposable { - private readonly _currentWidget = this._register(new MutableDisposable()); + private readonly _currentWidget = this._register(new MutableDisposable>()); constructor( private readonly _id: string, @@ -156,18 +152,20 @@ export class PostEditWidgetManager extends Disposable { )(() => this.clear())); } - public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet, canShowWidget: boolean, token: CancellationToken) { + public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise, token: CancellationToken) { const model = this._editor.getModel(); if (!model || !ranges.length) { return; } - const edit = edits.allEdits[edits.activeEditIndex]; + const edit = edits.allEdits.at(edits.activeEditIndex); if (!edit) { return; } - const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, edit); + const resolvedEdit = await resolve(edit, token); + + const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, resolvedEdit); // Use a decoration to track edits around the trigger range const primaryRange = ranges[0]; @@ -193,16 +191,16 @@ export class PostEditWidgetManager extends Disposable { } await model.undo(); - this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, token); + this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token); }); } } - public show(range: Range, edits: EditSet, onDidSelectEdit: (newIndex: number) => void) { + public show(range: Range, edits: EditSet, onDidSelectEdit: (newIndex: number) => void) { this.clear(); if (this._editor.hasModel()) { - this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit); + this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit); } } diff --git a/code/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts b/code/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts index f41f6866982..3013bcde756 100644 --- a/code/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts +++ b/code/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; 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 { +function createTestEdit(kind: string, args?: Partial): DocumentOnDropEdit { return { - label: '', + title: '', insertText: '', - providerId, + kind: new HierarchicalKind(kind), ...args, }; } @@ -21,48 +21,48 @@ function createTestEdit(providerId: string, args?: Partial): DropEdit suite('sortEditsByYieldTo', () => { test('Should noop for empty edits', () => { - const edits: DropEdit[] = []; + const edits: DocumentOnDropEdit[] = []; assert.deepStrictEqual(sortEditsByYieldTo(edits), []); }); test('Yielded to edit should get sorted after target', () => { - const edits: DropEdit[] = [ - createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('b') }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['b', 'a']); }); test('Should handle chain of yield to', () => { { - const edits: DropEdit[] = [ - createTestEdit('c', { yieldTo: [{ providerId: 'a' }] }), - createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('c', { yieldTo: [{ kind: new HierarchicalKind('a') }] }), + createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('b') }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a', 'c']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['b', 'a', 'c']); } { - const edits: DropEdit[] = [ - createTestEdit('a', { yieldTo: [{ providerId: 'b' }] }), - createTestEdit('c', { yieldTo: [{ providerId: 'a' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('b') }] }), + createTestEdit('c', { yieldTo: [{ kind: new HierarchicalKind('a') }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['b', 'a', 'c']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['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' }] }), + const edits: DocumentOnDropEdit[] = [ + createTestEdit('c', { yieldTo: [{ kind: new HierarchicalKind('x') }] }), + createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('y') }] }), createTestEdit('b'), ]; - assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.providerId), ['c', 'a', 'b']); + assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['c', 'a', 'b']); }); ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/code/src/vs/editor/contrib/find/browser/findOptionsWidget.ts b/code/src/vs/editor/contrib/find/browser/findOptionsWidget.ts index 7b693a1f28c..007723f6986 100644 --- a/code/src/vs/editor/contrib/find/browser/findOptionsWidget.ts +++ b/code/src/vs/editor/contrib/find/browser/findOptionsWidget.ts @@ -13,7 +13,7 @@ import { FIND_IDS } from 'vs/editor/contrib/find/browser/findModel'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export class FindOptionsWidget extends Widget implements IOverlayWidget { @@ -53,7 +53,7 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground), }; - const hoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + const hoverDelegate = this._register(createInstantHoverDelegate()); this.caseSensitive = this._register(new CaseSensitiveToggle({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand), diff --git a/code/src/vs/editor/contrib/find/browser/findWidget.ts b/code/src/vs/editor/contrib/find/browser/findWidget.ts index 76189e64741..8d80095419d 100644 --- a/code/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/code/src/vs/editor/contrib/find/browser/findWidget.ts @@ -43,9 +43,9 @@ import { isHighContrast } from 'vs/platform/theme/common/theme'; import { assertIsDefined } from 'vs/base/common/types'; import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Selection } from 'vs/editor/common/core/selection'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); const findCollapsedIcon = registerIcon('find-collapsed', Codicon.chevronRight, nls.localize('findCollapsedIcon', 'Icon to indicate that the editor find widget is collapsed.')); @@ -1014,7 +1014,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._updateMatchesCount(); // Create a scoped hover delegate for all find related buttons - const hoverDelegate = getDefaultHoverDelegate('element', true); + const hoverDelegate = this._register(createInstantHoverDelegate()); // Previous button this._prevBtn = this._register(new SimpleButton({ @@ -1051,6 +1051,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL icon: findSelectionIcon, title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand), isChecked: false, + hoverDelegate: hoverDelegate, inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground), inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground), @@ -1148,7 +1149,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL })); // Create scoped hover delegate for replace actions - const replaceHoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + const replaceHoverDelegate = this._register(createInstantHoverDelegate()); // Replace one button this._replaceBtn = this._register(new SimpleButton({ diff --git a/code/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts b/code/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts index ec127689cb0..bb76dd67d6c 100644 --- a/code/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts +++ b/code/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts @@ -162,12 +162,14 @@ export class FileReferencesRenderer implements ITreeRenderer, index: number, templateData: OneReferenceTemplate): void { templateData.set(node.element, node.filterData); } - disposeTemplate(): void { + disposeTemplate(templateData: OneReferenceTemplate): void { + templateData.dispose(); } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts index 42d0404984f..21abd0b7093 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts @@ -5,8 +5,10 @@ import { equals } from 'vs/base/common/arrays'; import { splitLines } from 'vs/base/common/strings'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ColumnRange, applyEdits } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { ColumnRange } from 'vs/editor/contrib/inlineCompletions/browser/utils'; export class GhostText { constructor( @@ -25,13 +27,12 @@ export class GhostText { * Only used for testing/debugging. */ render(documentText: string, debug: boolean = false): string { - const l = this.lineNumber; - return applyEdits(documentText, [ - ...this.parts.map(p => ({ - range: { startLineNumber: l, endLineNumber: l, startColumn: p.column, endColumn: p.column }, - text: debug ? `[${p.lines.join('\n')}]` : p.lines.join('\n') - })), - ]); + return new TextEdit([ + ...this.parts.map(p => new SingleTextEdit( + Range.fromPositions(new Position(this.lineNumber, p.column)), + debug ? `[${p.lines.join('\n')}]` : p.lines.join('\n') + )), + ]).applyToString(documentText); } renderForScreenReader(lineText: string): string { @@ -41,12 +42,12 @@ export class GhostText { const lastPart = this.parts[this.parts.length - 1]; const cappedLineText = lineText.substr(0, lastPart.column - 1); - const text = applyEdits(cappedLineText, - this.parts.map(p => ({ - range: { startLineNumber: 1, endLineNumber: 1, startColumn: p.column, endColumn: p.column }, - text: p.lines.join('\n') - })) - ); + const text = new TextEdit([ + ...this.parts.map(p => new SingleTextEdit( + Range.fromPositions(new Position(1, p.column)), + p.lines.join('\n') + )), + ]).applyToString(cappedLineText); return text.substring(this.parts[0].column - 1); } @@ -106,14 +107,14 @@ export class GhostTextReplacement { const replaceRange = this.columnRange.toRange(this.lineNumber); if (debug) { - return applyEdits(documentText, [ - { range: Range.fromPositions(replaceRange.getStartPosition()), text: `(` }, - { range: Range.fromPositions(replaceRange.getEndPosition()), text: `)[${this.newLines.join('\n')}]` } - ]); + return new TextEdit([ + new SingleTextEdit(Range.fromPositions(replaceRange.getStartPosition()), '('), + new SingleTextEdit(Range.fromPositions(replaceRange.getEndPosition()), `)[${this.newLines.join('\n')}]`), + ]).applyToString(documentText); } else { - return applyEdits(documentText, [ - { range: replaceRange, text: this.newLines.join('\n') } - ]); + return new TextEdit([ + new SingleTextEdit(replaceRange, this.newLines.join('\n')), + ]).applyToString(documentText); } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts index 30f622e01fc..e847839c042 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts @@ -302,7 +302,7 @@ class StatusBarViewItem extends MenuEntryActionViewItem { if (this.label) { const div = h('div.keybinding').root; - const k = new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions }); + const k = this._register(new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions })); k.set(kb); this.label.textContent = this._action.label; this.label.appendChild(div); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 006c9873c52..996471bde48 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Permutation } from 'vs/base/common/arrays'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -14,18 +15,20 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { InlineCompletionContext, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from 'vs/editor/common/languages'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; import { SuggestItemInfo } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; -import { Permutation, addPositions, getNewRanges, lengthOfText, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { addPositions, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { singleTextEditAugments, computeGhostText, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { TextLength } from 'vs/editor/common/core/textLength'; export enum VersionIdChangeReason { Undo, @@ -212,7 +215,7 @@ export class InlineCompletionsModel extends Disposable { const suggestItem = this.selectedSuggestItem.read(reader); if (suggestItem) { - const suggestCompletionEdit = suggestItem.toSingleTextEdit().removeCommonPrefix(model); + const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.toSingleTextEdit(), model); const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader); @@ -225,7 +228,7 @@ export class InlineCompletionsModel extends Disposable { const positions = this._positions.read(reader); const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; const ghostTexts = edits - .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], fullEditPreviewLength)) + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength)) .filter(isDefined); const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); return { edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; @@ -239,7 +242,7 @@ export class InlineCompletionsModel extends Disposable { const positions = this._positions.read(reader); const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; const ghostTexts = edits - .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], 0)) + .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], 0)) .filter(isDefined); if (!ghostTexts[0]) { return undefined; } return { edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; @@ -255,8 +258,8 @@ export class InlineCompletionsModel extends Disposable { const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => { let r = completion.toSingleTextEdit(reader); - r = r.removeCommonPrefix(model, Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())); - return r.augments(suggestCompletion) ? { completion, edit: r } : undefined; + r = singleTextRemoveCommonPrefix(r, model, Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())); + return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined; }); return augmentedCompletion; @@ -379,7 +382,7 @@ export class InlineCompletionsModel extends Disposable { } } return acceptUntilIndexExclusive; - }); + }, PartialAcceptTriggerKind.Word); } public async acceptNextLine(editor: ICodeEditor): Promise { @@ -389,10 +392,10 @@ export class InlineCompletionsModel extends Disposable { return m.index + 1; } return text.length; - }); + }, PartialAcceptTriggerKind.Line); } - private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number): Promise { + private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } @@ -441,13 +444,16 @@ export class InlineCompletionsModel extends Disposable { } if (completion.source.provider.handlePartialAccept) { - const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), addPositions(ghostTextPos, lengthOfText(partialGhostTextVal))); + const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); // This assumes that the inline completion and the model use the same EOL style. const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF); completion.source.provider.handlePartialAccept( completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, + { + kind, + } ); } } finally { @@ -456,7 +462,7 @@ export class InlineCompletionsModel extends Disposable { } public handleSuggestAccepted(item: SuggestItemInfo) { - const itemEdit = item.toSingleTextEdit().removeCommonPrefix(this.textModel); + const itemEdit = singleTextRemoveCommonPrefix(item.toSingleTextEdit(), this.textModel); const augmentedCompletion = this._computeAugmentation(itemEdit, undefined); if (!augmentedCompletion) { return; } @@ -465,6 +471,9 @@ export class InlineCompletionsModel extends Disposable { inlineCompletion.source.inlineCompletions, inlineCompletion.sourceInlineCompletion, itemEdit.text.length, + { + kind: PartialAcceptTriggerKind.Suggest, + } ); } } @@ -512,7 +521,8 @@ function substringPos(text: string, pos: Position): string { function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range)); - const sortedNewRanges = getNewRanges(sortPerm.apply(edits)); + const edit = new TextEdit(sortPerm.apply(edits)); + const sortedNewRanges = edit.getNewRanges(); const newRanges = sortPerm.inverse().apply(sortedNewRanges); return newRanges.map(range => range.getEndPosition()); } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts index 3e38e9e8161..94a8b33d477 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts @@ -15,7 +15,8 @@ import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; +import { singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class InlineCompletionsSource extends Disposable { private readonly _updateOperation = this._register(new MutableDisposable()); @@ -282,7 +283,7 @@ export class InlineCompletionWithUpdatedRange { } public isVisible(model: ITextModel, cursorPosition: Position, reader: IReader | undefined): boolean { - const minimizedReplacement = this._toFilterTextReplacement(reader).removeCommonPrefix(model); + const minimizedReplacement = singleTextRemoveCommonPrefix(this._toFilterTextReplacement(reader), model); if ( !this._isValid diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts index 9d91e0ade1e..28052040c32 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts @@ -17,7 +17,7 @@ import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionPro import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ITextModel } from 'vs/editor/common/model'; import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { SnippetParser, Text } from 'vs/editor/contrib/snippet/browser/snippetParser'; diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts index 8517f24ec85..750eb459829 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts @@ -7,147 +7,136 @@ import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff'; import { commonPrefixLength, getLeadingWhitespace } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { TextLength } from 'vs/editor/common/core/textLength'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; -import { addPositions, lengthOfText } from 'vs/editor/contrib/inlineCompletions/browser/utils'; -export class SingleTextEdit { - constructor( - public readonly range: Range, - public readonly text: string - ) { +export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextModel, validModelRange?: Range): SingleTextEdit { + const modelRange = validModelRange ? edit.range.intersectRanges(validModelRange) : edit.range; + if (!modelRange) { + return edit; } + const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); + const commonPrefixLen = commonPrefixLength(valueToReplace, edit.text); + const start = TextLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); + const text = edit.text.substring(commonPrefixLen); + const range = Range.fromPositions(start, edit.range.getEndPosition()); + return new SingleTextEdit(range, text); +} - static equals(first: SingleTextEdit, second: SingleTextEdit) { - return first.range.equalsRange(second.range) && first.text === second.text; +export function singleTextEditAugments(edit: SingleTextEdit, base: SingleTextEdit): boolean { + // The augmented completion must replace the base range, but can replace even more + return edit.text.startsWith(base.text) && rangeExtends(edit.range, base.range); +} +/** + * @param previewSuffixLength Sets where to split `inlineCompletion.text`. + * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`. +*/ +export function computeGhostText( + edit: SingleTextEdit, + model: ITextModel, + mode: 'prefix' | 'subword' | 'subwordSmart', + cursorPosition?: Position, + previewSuffixLength = 0 +): GhostText | undefined { + let e = singleTextRemoveCommonPrefix(edit, model); + + if (e.range.endLineNumber !== e.range.startLineNumber) { + // This edit might span multiple lines, but the first lines must be a common prefix. + return undefined; } - removeCommonPrefix(model: ITextModel, validModelRange?: Range): SingleTextEdit { - const modelRange = validModelRange ? this.range.intersectRanges(validModelRange) : this.range; - if (!modelRange) { - return this; - } - const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); - const commonPrefixLen = commonPrefixLength(valueToReplace, this.text); - const start = addPositions(this.range.getStartPosition(), lengthOfText(valueToReplace.substring(0, commonPrefixLen))); - const text = this.text.substring(commonPrefixLen); - const range = Range.fromPositions(start, this.range.getEndPosition()); - return new SingleTextEdit(range, text); - } + const sourceLine = model.getLineContent(e.range.startLineNumber); + const sourceIndentationLength = getLeadingWhitespace(sourceLine).length; - augments(base: SingleTextEdit): boolean { - // The augmented completion must replace the base range, but can replace even more - return this.text.startsWith(base.text) && rangeExtends(this.range, base.range); - } + const suggestionTouchesIndentation = e.range.startColumn - 1 <= sourceIndentationLength; + if (suggestionTouchesIndentation) { + // source: ··········[······abc] + // ^^^^^^^^^ inlineCompletion.range + // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength + // ^^^^^^ replacedIndentation.length + // ^^^ rangeThatDoesNotReplaceIndentation - /** - * @param previewSuffixLength Sets where to split `inlineCompletion.text`. - * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`. - */ - computeGhostText( - model: ITextModel, - mode: 'prefix' | 'subword' | 'subwordSmart', - cursorPosition?: Position, - previewSuffixLength = 0 - ): GhostText | undefined { - let edit = this.removeCommonPrefix(model); - - if (edit.range.endLineNumber !== edit.range.startLineNumber) { - // This edit might span multiple lines, but the first lines must be a common prefix. - return undefined; - } + // inlineCompletion.text: '··foo' + // ^^ suggestionAddedIndentationLength - const sourceLine = model.getLineContent(edit.range.startLineNumber); - const sourceIndentationLength = getLeadingWhitespace(sourceLine).length; + const suggestionAddedIndentationLength = getLeadingWhitespace(e.text).length; - const suggestionTouchesIndentation = edit.range.startColumn - 1 <= sourceIndentationLength; - if (suggestionTouchesIndentation) { - // source: ··········[······abc] - // ^^^^^^^^^ inlineCompletion.range - // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength - // ^^^^^^ replacedIndentation.length - // ^^^ rangeThatDoesNotReplaceIndentation + const replacedIndentation = sourceLine.substring(e.range.startColumn - 1, sourceIndentationLength); - // inlineCompletion.text: '··foo' - // ^^ suggestionAddedIndentationLength + const [startPosition, endPosition] = [e.range.getStartPosition(), e.range.getEndPosition()]; + const newStartPosition = + startPosition.column + replacedIndentation.length <= endPosition.column + ? startPosition.delta(0, replacedIndentation.length) + : endPosition; + const rangeThatDoesNotReplaceIndentation = Range.fromPositions(newStartPosition, endPosition); - const suggestionAddedIndentationLength = getLeadingWhitespace(edit.text).length; + const suggestionWithoutIndentationChange = + e.text.startsWith(replacedIndentation) + // Adds more indentation without changing existing indentation: We can add ghost text for this + ? e.text.substring(replacedIndentation.length) + // Changes or removes existing indentation. Only add ghost text for the non-indentation part. + : e.text.substring(suggestionAddedIndentationLength); - const replacedIndentation = sourceLine.substring(edit.range.startColumn - 1, sourceIndentationLength); + e = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange); + } - const [startPosition, endPosition] = [edit.range.getStartPosition(), edit.range.getEndPosition()]; - const newStartPosition = - startPosition.column + replacedIndentation.length <= endPosition.column - ? startPosition.delta(0, replacedIndentation.length) - : endPosition; - const rangeThatDoesNotReplaceIndentation = Range.fromPositions(newStartPosition, endPosition); + // This is a single line string + const valueToBeReplaced = model.getValueInRange(e.range); - const suggestionWithoutIndentationChange = - edit.text.startsWith(replacedIndentation) - // Adds more indentation without changing existing indentation: We can add ghost text for this - ? edit.text.substring(replacedIndentation.length) - // Changes or removes existing indentation. Only add ghost text for the non-indentation part. - : edit.text.substring(suggestionAddedIndentationLength); + const changes = cachingDiff(valueToBeReplaced, e.text); - edit = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange); - } + if (!changes) { + // No ghost text in case the diff would be too slow to compute + return undefined; + } - // This is a single line string - const valueToBeReplaced = model.getValueInRange(edit.range); + const lineNumber = e.range.startLineNumber; - const changes = cachingDiff(valueToBeReplaced, edit.text); + const parts = new Array(); - if (!changes) { - // No ghost text in case the diff would be too slow to compute + if (mode === 'prefix') { + const filteredChanges = changes.filter(c => c.originalLength === 0); + if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) { + // Prefixes only have a single change. return undefined; } + } - const lineNumber = edit.range.startLineNumber; + const previewStartInCompletionText = e.text.length - previewSuffixLength; - const parts = new Array(); + for (const c of changes) { + const insertColumn = e.range.startColumn + c.originalStart + c.originalLength; - if (mode === 'prefix') { - const filteredChanges = changes.filter(c => c.originalLength === 0); - if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) { - // Prefixes only have a single change. - return undefined; - } + if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === e.range.startLineNumber && insertColumn < cursorPosition.column) { + // No ghost text before cursor + return undefined; } - const previewStartInCompletionText = edit.text.length - previewSuffixLength; - - for (const c of changes) { - const insertColumn = edit.range.startColumn + c.originalStart + c.originalLength; - - if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === edit.range.startLineNumber && insertColumn < cursorPosition.column) { - // No ghost text before cursor - return undefined; - } - - if (c.originalLength > 0) { - return undefined; - } + if (c.originalLength > 0) { + return undefined; + } - if (c.modifiedLength === 0) { - continue; - } + if (c.modifiedLength === 0) { + continue; + } - const modifiedEnd = c.modifiedStart + c.modifiedLength; - const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText)); - const nonPreviewText = edit.text.substring(c.modifiedStart, nonPreviewTextEnd); - const italicText = edit.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd)); + const modifiedEnd = c.modifiedStart + c.modifiedLength; + const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText)); + const nonPreviewText = e.text.substring(c.modifiedStart, nonPreviewTextEnd); + const italicText = e.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd)); - if (nonPreviewText.length > 0) { - parts.push(new GhostTextPart(insertColumn, nonPreviewText, false)); - } - if (italicText.length > 0) { - parts.push(new GhostTextPart(insertColumn, italicText, true)); - } + if (nonPreviewText.length > 0) { + parts.push(new GhostTextPart(insertColumn, nonPreviewText, false)); + } + if (italicText.length > 0) { + parts.push(new GhostTextPart(insertColumn, italicText, true)); } - - return new GhostText(lineNumber, parts); } + + return new GhostText(lineNumber, parts); } function rangeExtends(extendingRange: Range, rangeToExtend: Range): boolean { diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts index 90d53b26c8f..5f98b45033a 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts @@ -14,10 +14,11 @@ import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { IObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { ITextModel } from 'vs/editor/common/model'; import { compareBy, numberComparator } from 'vs/base/common/arrays'; import { findFirstMaxBy } from 'vs/base/common/arraysFind'; +import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class SuggestWidgetAdaptor extends Disposable { private isSuggestWidgetVisible: boolean = false; @@ -66,7 +67,8 @@ export class SuggestWidgetAdaptor extends Disposable { return -1; } - const itemToPreselect = this.suggestControllerPreselector()?.removeCommonPrefix(textModel); + const i = this.suggestControllerPreselector(); + const itemToPreselect = i ? singleTextRemoveCommonPrefix(i, textModel) : undefined; if (!itemToPreselect) { return -1; } @@ -75,8 +77,8 @@ export class SuggestWidgetAdaptor extends Disposable { const candidates = suggestItems .map((suggestItem, index) => { const suggestItemInfo = SuggestItemInfo.fromSuggestion(suggestController, textModel, position, suggestItem, this.isShiftKeyPressed); - const suggestItemTextEdit = suggestItemInfo.toSingleTextEdit().removeCommonPrefix(textModel); - const valid = itemToPreselect.augments(suggestItemTextEdit); + const suggestItemTextEdit = singleTextRemoveCommonPrefix(suggestItemInfo.toSingleTextEdit(), textModel); + const valid = singleTextEditAugments(itemToPreselect, suggestItemTextEdit); return { index, valid, prefixLength: suggestItemTextEdit.text.length, suggestItem }; }) .filter(item => item && item.valid && item.prefixLength > 0); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 4a9e78238b6..20236aade3b 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -3,54 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareBy } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IObservable, autorunOpts } from 'vs/base/common/observable'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; - -export function applyEdits(text: string, edits: { range: IRange; text: string }[]): string { - const transformer = new PositionOffsetTransformer(text); - const offsetEdits = edits.map(e => { - const range = Range.lift(e.range); - return ({ - startOffset: transformer.getOffset(range.getStartPosition()), - endOffset: transformer.getOffset(range.getEndPosition()), - text: e.text - }); - }); - - offsetEdits.sort((a, b) => b.startOffset - a.startOffset); - - for (const edit of offsetEdits) { - text = text.substring(0, edit.startOffset) + edit.text + text.substring(edit.endOffset); - } - - return text; -} - -class PositionOffsetTransformer { - private readonly lineStartOffsetByLineIdx: number[]; - - constructor(text: string) { - this.lineStartOffsetByLineIdx = []; - this.lineStartOffsetByLineIdx.push(0); - for (let i = 0; i < text.length; i++) { - if (text.charAt(i) === '\n') { - this.lineStartOffsetByLineIdx.push(i + 1); - } - } - } - - getOffset(position: Position): number { - return this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1; - } -} const array: ReadonlyArray = []; export function getReadonlyEmptyArray(): readonly T[] { @@ -99,86 +58,3 @@ export function addPositions(pos1: Position, pos2: Position): Position { export function subtractPositions(pos1: Position, pos2: Position): Position { return new Position(pos1.lineNumber - pos2.lineNumber + 1, pos1.lineNumber - pos2.lineNumber === 0 ? pos1.column - pos2.column + 1 : pos1.column); } - -export function lengthOfText(text: string): Position { - let line = 1; - let column = 1; - for (const c of text) { - if (c === '\n') { - line++; - column = 1; - } else { - column++; - } - } - return new Position(line, column); -} - -/** - * Given some text edits, this function finds the new ranges of the editted text post application of all edits. - * Assumes that the edit ranges are disjoint and they are sorted in the order of the ranges - * @param edits edits applied - * @returns new ranges post edits for every edit - */ -export function getNewRanges(edits: ISingleEditOperation[]): Range[] { - const newRanges: Range[] = []; - let previousEditEndLineNumber = 0; - let lineOffset = 0; - let columnOffset = 0; - - for (const edit of edits) { - const text = edit.text ?? ''; - const textLength = lengthOfText(text); - const newRangeStart = Position.lift({ - lineNumber: edit.range.startLineNumber + lineOffset, - column: edit.range.startColumn + (edit.range.startLineNumber === previousEditEndLineNumber ? columnOffset : 0) - }); - const newRangeEnd = addPositions( - newRangeStart, - textLength - ); - newRanges.push(Range.fromPositions(newRangeStart, newRangeEnd)); - lineOffset += textLength.lineNumber - edit.range.endLineNumber + edit.range.startLineNumber - 1; - columnOffset = newRangeEnd.column - edit.range.endColumn; - previousEditEndLineNumber = edit.range.endLineNumber; - } - return newRanges; -} - -/** - * Given a text model and edits, this function finds the inverse text edits - * @param model model on which to apply the edits - * @param edits edits applied - * @returns inverse edits - */ -export function inverseEdits(model: TextModel, edits: ISingleEditOperation[]): ISingleEditOperation[] { - const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); - const sortedRanges = getNewRanges(sortPerm.apply(edits)); - const newRanges = sortPerm.inverse().apply(sortedRanges); - const inverseEdits: ISingleEditOperation[] = []; - for (let i = 0; i < edits.length; i++) { - inverseEdits.push({ range: newRanges[i], text: model.getValueInRange(edits[i].range) }); - } - return inverseEdits; -} - -export class Permutation { - constructor(private readonly _indexMap: number[]) { } - - public static createSortPermutation(arr: readonly T[], compareFn: (a: T, b: T) => number): Permutation { - const sortIndices = Array.from(arr.keys()).sort((index1, index2) => compareFn(arr[index1], arr[index2])); - return new Permutation(sortIndices); - } - - apply(arr: readonly T[]): T[] { - return arr.map((_, index) => arr[this._indexMap[index]]); - } - - inverse(): Permutation { - const inverseIndexMap = this._indexMap.slice(); - for (let i = 0; i < this._indexMap.length; i++) { - inverseIndexMap[this._indexMap[i]] = i; - } - return new Permutation(inverseIndexMap); - } -} diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index 67c56bb3945..648f5940596 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { getSecondaryEdits } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { Range } from 'vs/editor/common/core/range'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index 4c846025949..e160c3daa01 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -15,13 +15,14 @@ import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeatu import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; -import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { GhostTextContext, MockInlineCompletionsProvider } from 'vs/editor/contrib/inlineCompletions/test/browser/utils'; import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { Selection } from 'vs/editor/common/core/selection'; +import { computeGhostText } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; suite('Inline Completions', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -37,7 +38,7 @@ suite('Inline Completions', () => { const options = ['prefix', 'subword'] as const; const result = {} as any; for (const option of options) { - result[option] = new SingleTextEdit(range, suggestion).computeGhostText(tempModel, option)?.render(cleanedText, true); + result[option] = computeGhostText(new SingleTextEdit(range, suggestion), tempModel, option)?.render(cleanedText, true); } tempModel.dispose(); diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts deleted file mode 100644 index 16c918fbe18..00000000000 --- a/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { MersenneTwister, getRandomEditInfos, toEdit, } from 'vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test'; -import { inverseEdits } from 'vs/editor/contrib/inlineCompletions/browser/utils'; -import { generateRandomMultilineString } from 'vs/editor/contrib/inlineCompletions/test/browser/utils'; - -suite('getNewRanges', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - for (let seed = 0; seed < 20; seed++) { - test(`test ${seed}`, () => { - const rng = new MersenneTwister(seed); - const randomText = generateRandomMultilineString(rng, 10); - const model = createTextModel(randomText); - - const edits = getRandomEditInfos(model, rng.nextIntRange(1, 4), rng, true).map(e => toEdit(e)); - const invEdits = inverseEdits(model, edits); - - model.applyEdits(edits); - model.applyEdits(invEdits); - - assert.deepStrictEqual(model.getValue(), randomText); - model.dispose(); - }); - } - -}); diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 49e09d4e4a7..11c24b0b0e6 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -13,7 +13,6 @@ import { InlineCompletion, InlineCompletionContext, InlineCompletionsProvider } import { ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { autorun } from 'vs/base/common/observable'; -import { MersenneTwister } from 'vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test'; export class MockInlineCompletionsProvider implements InlineCompletionsProvider { private returnValue: InlineCompletion[] = []; @@ -133,23 +132,3 @@ export class GhostTextContext extends Disposable { } } -export function generateRandomMultilineString(rng: MersenneTwister, numberOfLines: number, maximumLengthOfLines: number = 20): string { - let randomText: string = ''; - for (let i = 0; i < numberOfLines; i++) { - const lengthOfLine = rng.nextIntRange(0, maximumLengthOfLines + 1); - randomText += generateRandomSimpleString(rng, lengthOfLine) + '\n'; - } - return randomText; -} - -function generateRandomSimpleString(rng: MersenneTwister, stringLength: number): string { - const possibleCharacters: string = ' abcdefghijklmnopqrstuvwxyz0123456789'; - let randomText: string = ''; - for (let i = 0; i < stringLength; i++) { - const characterIndex = rng.nextIntRange(0, possibleCharacters.length); - randomText += possibleCharacters.charAt(characterIndex); - - } - return randomText; -} - diff --git a/code/src/vs/editor/contrib/inlineEdit/browser/commands.ts b/code/src/vs/editor/contrib/inlineEdit/browser/commands.ts index 11d6dfa2f22..d7e1f4fe8cb 100644 --- a/code/src/vs/editor/contrib/inlineEdit/browser/commands.ts +++ b/code/src/vs/editor/contrib/inlineEdit/browser/commands.ts @@ -37,7 +37,7 @@ export class AcceptInlineEdit extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineEditController.get(editor); - controller?.accept(); + await controller?.accept(); } } @@ -147,7 +147,7 @@ export class RejectInlineEdit extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineEditController.get(editor); - controller?.clear(); + await controller?.clear(); } } diff --git a/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 8355ef312cf..4e0c10eb335 100644 --- a/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { ISettableObservable, autorun, constObservable, disposableObservableValue, observableFromEvent, observableSignalFromEvent } from 'vs/base/common/observable'; +import { ISettableObservable, autorun, constObservable, disposableObservableValue, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; @@ -21,6 +21,7 @@ import { InlineEditHintsWidget } from 'vs/editor/contrib/inlineEdit/browser/inli import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { createStyleSheet2 } from 'vs/base/browser/dom'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; export class InlineEditWidget implements IDisposable { constructor(public readonly widget: GhostTextWidget, public readonly edit: IInlineEdit) { } @@ -49,7 +50,7 @@ export class InlineEditController extends Disposable { private _currentRequestCts: CancellationTokenSource | undefined; private _jumpBackPosition: Position | undefined; - private _isAccepting: boolean = false; + private _isAccepting: ISettableObservable = observableValue(this, false); private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); @@ -76,6 +77,9 @@ export class InlineEditController extends Disposable { return; } modelChangedSignal.read(reader); + if (this._isAccepting.read(reader)) { + return; + } this.getInlineEdit(editor, true); })); @@ -111,7 +115,7 @@ export class InlineEditController extends Disposable { //Clear suggestions on lost focus const editorBlurSingal = observableSignalFromEvent('InlineEditController.editorBlurSignal', editor.onDidBlurEditorWidget); - this._register(autorun(reader => { + this._register(autorun(async reader => { /** @description InlineEditController.editorBlur */ if (!this._enabled.read(reader)) { return; @@ -121,9 +125,9 @@ export class InlineEditController extends Disposable { if (this._configurationService.getValue('editor.experimentalInlineEdit.keepOnBlur') || editor.getOption(EditorOption.inlineEdit).keepOnBlur) { return; } - this._currentRequestCts?.dispose(); + this._currentRequestCts?.dispose(true); this._currentRequestCts = undefined; - this.clear(false); + await this.clear(false); })); //Invoke provider on focus @@ -222,8 +226,7 @@ export class InlineEditController extends Disposable { private async getInlineEdit(editor: ICodeEditor, auto: boolean) { this._isCursorAtInlineEditContext.set(false); - this.clear(); - this._isAccepting = false; + await this.clear(); const edit = await this.fetchInlineEdit(editor, auto); if (!edit) { return; @@ -254,8 +257,8 @@ export class InlineEditController extends Disposable { this.editor.revealPositionInCenterIfOutsideViewport(this._jumpBackPosition); } - public accept(): void { - this._isAccepting = true; + public async accept() { + this._isAccepting.set(true, undefined); const data = this._currentEdit.get()?.edit; if (!data) { return; @@ -269,10 +272,15 @@ export class InlineEditController extends Disposable { this.editor.pushUndoStop(); this.editor.executeEdits('acceptCurrent', [EditOperation.replace(Range.lift(data.range), text)]); if (data.accepted) { - this._commandService.executeCommand(data.accepted.id, ...data.accepted.arguments || []); + await this._commandService + .executeCommand(data.accepted.id, ...(data.accepted.arguments || [])) + .then(undefined, onUnexpectedExternalError); } this.freeEdit(data); - this._currentEdit.set(undefined, undefined); + transaction((tx) => { + this._currentEdit.set(undefined, tx); + this._isAccepting.set(false, tx); + }); } public jumpToCurrent(): void { @@ -288,10 +296,12 @@ export class InlineEditController extends Disposable { this.editor.revealPositionInCenterIfOutsideViewport(position); } - public clear(sendRejection: boolean = true) { + public async clear(sendRejection: boolean = true) { const edit = this._currentEdit.get()?.edit; - if (edit && edit?.rejected && !this._isAccepting && sendRejection) { - this._commandService.executeCommand(edit.rejected.id, ...edit.rejected.arguments || []); + if (edit && edit?.rejected && sendRejection) { + await this._commandService + .executeCommand(edit.rejected.id, ...(edit.rejected.arguments || [])) + .then(undefined, onUnexpectedExternalError); } if (edit) { this.freeEdit(edit); diff --git a/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts b/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts index 73824bd5e6c..59553805863 100644 --- a/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts +++ b/code/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts @@ -175,7 +175,7 @@ class StatusBarViewItem extends MenuEntryActionViewItem { if (this.label) { const div = h('div.keybinding').root; - const k = new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions }); + const k = this._register(new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions })); k.set(kb); this.label.textContent = this._action.label; this.label.appendChild(div); diff --git a/code/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/code/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 74d7849587e..45b3fafba71 100644 --- a/code/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/code/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -25,6 +25,7 @@ import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; // copy lines @@ -243,7 +244,16 @@ export abstract class AbstractSortLinesAction extends EditorAction { } public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { - const selections = editor.getSelections() || []; + if (!editor.hasModel()) { + return; + } + + const model = editor.getModel(); + let selections = editor.getSelections(); + if (selections.length === 1 && selections[0].isEmpty()) { + // Apply to whole document. + selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))]; + } for (const selection of selections) { if (!SortLinesCommand.canRun(editor.getModel(), selection, this.descending)) { @@ -308,8 +318,16 @@ export class DeleteDuplicateLinesAction extends EditorAction { const endCursorState: Selection[] = []; let linesDeleted = 0; + let updateSelection = true; + + let selections = editor.getSelections(); + if (selections.length === 1 && selections[0].isEmpty()) { + // Apply to whole document. + selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))]; + updateSelection = false; + } - for (const selection of editor.getSelections()) { + for (const selection of selections) { const uniqueLines = new Set(); const lines = []; @@ -347,7 +365,7 @@ export class DeleteDuplicateLinesAction extends EditorAction { } editor.pushUndoStop(); - editor.executeEdits(this.id, edits, endCursorState); + editor.executeEdits(this.id, edits, updateSelection ? endCursorState : undefined); editor.pushUndoStop(); } } @@ -385,7 +403,11 @@ export class TrimTrailingWhitespaceAction extends EditorAction { return; } - const command = new TrimTrailingWhitespaceCommand(selection, cursors); + const config = _accessor.get(IConfigurationService); + const model = editor.getModel(); + const trimInRegexAndStrings = config.getValue('files.trimTrailingWhitespaceInRegexAndStrings', { overrideIdentifier: model?.getLanguageId(), resource: model?.uri }); + + const command = new TrimTrailingWhitespaceCommand(selection, cursors, trimInRegexAndStrings); editor.pushUndoStop(); editor.executeCommands(this.id, [command]); @@ -1187,6 +1209,35 @@ export class CamelCaseAction extends AbstractCaseAction { } } +export class PascalCaseAction extends AbstractCaseAction { + public static wordBoundary = new BackwardsCompatibleRegExp('[_\\s-]', 'gm'); + public static wordBoundaryToMaintain = new BackwardsCompatibleRegExp('(?<=\\.)', 'gm'); + + constructor() { + super({ + id: 'editor.action.transformToPascalcase', + label: nls.localize('editor.transformToPascalcase', "Transform to Pascal Case"), + alias: 'Transform to Pascal Case', + precondition: EditorContextKeys.writable + }); + } + + protected _modifyText(text: string, wordSeparators: string): string { + const wordBoundary = PascalCaseAction.wordBoundary.get(); + const wordBoundaryToMaintain = PascalCaseAction.wordBoundaryToMaintain.get(); + + if (!wordBoundary || !wordBoundaryToMaintain) { + // cannot support this + return text; + } + + const wordsWithMaintainBoundaries = text.split(wordBoundaryToMaintain); + const words = wordsWithMaintainBoundaries.map((word: string) => word.split(wordBoundary)).flat(); + return words.map((word: string) => word.substring(0, 1).toLocaleUpperCase() + word.substring(1)) + .join(''); + } +} + export class KebabCaseAction extends AbstractCaseAction { public static isSupported(): boolean { @@ -1257,6 +1308,9 @@ if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters. if (CamelCaseAction.wordBoundary.isSupported()) { registerEditorAction(CamelCaseAction); } +if (PascalCaseAction.wordBoundary.isSupported()) { + registerEditorAction(PascalCaseAction); +} if (TitleCaseAction.titleBoundary.isSupported()) { registerEditorAction(TitleCaseAction); } diff --git a/code/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts b/code/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts index 3df2a1f682c..5425697a2e4 100644 --- a/code/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts +++ b/code/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts @@ -12,7 +12,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; -import { CamelCaseAction, DeleteAllLeftAction, DeleteAllRightAction, DeleteDuplicateLinesAction, DeleteLinesAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, KebabCaseAction, LowerCaseAction, SnakeCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TitleCaseAction, TransposeAction, UpperCaseAction } from 'vs/editor/contrib/linesOperations/browser/linesOperations'; +import { CamelCaseAction, PascalCaseAction, DeleteAllLeftAction, DeleteAllRightAction, DeleteDuplicateLinesAction, DeleteLinesAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, KebabCaseAction, LowerCaseAction, SnakeCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TitleCaseAction, TransposeAction, UpperCaseAction } from 'vs/editor/contrib/linesOperations/browser/linesOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; @@ -53,6 +53,25 @@ suite('Editor Contrib - Line Operations', () => { }); }); + test('should sort lines in ascending order', function () { + withTestCodeEditor( + [ + 'omicron', + 'beta', + 'alpha' + ], {}, (editor) => { + const model = editor.getModel()!; + const sortLinesAscendingAction = new SortLinesAscendingAction(); + + executeAction(sortLinesAscendingAction, editor); + assert.deepStrictEqual(model.getLinesContent(), [ + 'alpha', + 'beta', + 'omicron' + ]); + }); + }); + test('should sort multiple selections in ascending order', function () { withTestCodeEditor( [ @@ -148,7 +167,7 @@ suite('Editor Contrib - Line Operations', () => { }); suite('DeleteDuplicateLinesAction', () => { - test('should remove duplicate lines', function () { + test('should remove duplicate lines within selection', function () { withTestCodeEditor( [ 'alpha', @@ -172,6 +191,29 @@ suite('Editor Contrib - Line Operations', () => { }); }); + test('should remove duplicate lines', function () { + withTestCodeEditor( + [ + 'alpha', + 'beta', + 'beta', + 'beta', + 'alpha', + 'omicron', + ], {}, (editor) => { + const model = editor.getModel()!; + const deleteDuplicateLinesAction = new DeleteDuplicateLinesAction(); + + executeAction(deleteDuplicateLinesAction, editor); + assert.deepStrictEqual(model.getLinesContent(), [ + 'alpha', + 'beta', + 'omicron', + ]); + assert.ok(editor.getSelection().isEmpty()); + }); + }); + test('should remove duplicate lines in multiple selections', function () { withTestCodeEditor( [ @@ -935,6 +977,74 @@ suite('Editor Contrib - Line Operations', () => { assertSelection(editor, new Selection(11, 1, 11, 11)); } ); + + withTestCodeEditor( + [ + 'hello world', + 'öçşğü', + 'parseHTMLString', + 'getElementById', + 'PascalCase', + 'öçşÖÇŞğüĞÜ', + 'audioConverter.convertM4AToMP3();', + 'Capital_Snake_Case', + 'parseHTML4String', + 'Kebab-Case', + ], {}, (editor) => { + const model = editor.getModel()!; + const pascalCaseAction = new PascalCaseAction(); + + editor.setSelection(new Selection(1, 1, 1, 12)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(1), 'HelloWorld'); + assertSelection(editor, new Selection(1, 1, 1, 11)); + + editor.setSelection(new Selection(2, 1, 2, 6)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(2), 'Öçşğü'); + assertSelection(editor, new Selection(2, 1, 2, 6)); + + editor.setSelection(new Selection(3, 1, 3, 16)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(3), 'ParseHTMLString'); + assertSelection(editor, new Selection(3, 1, 3, 16)); + + editor.setSelection(new Selection(4, 1, 4, 15)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(4), 'GetElementById'); + assertSelection(editor, new Selection(4, 1, 4, 15)); + + editor.setSelection(new Selection(5, 1, 5, 11)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(5), 'PascalCase'); + assertSelection(editor, new Selection(5, 1, 5, 11)); + + editor.setSelection(new Selection(6, 1, 6, 11)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(6), 'ÖçşÖÇŞğüĞÜ'); + assertSelection(editor, new Selection(6, 1, 6, 11)); + + editor.setSelection(new Selection(7, 1, 7, 34)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(7), 'AudioConverter.ConvertM4AToMP3();'); + assertSelection(editor, new Selection(7, 1, 7, 34)); + + editor.setSelection(new Selection(8, 1, 8, 19)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(8), 'CapitalSnakeCase'); + assertSelection(editor, new Selection(8, 1, 8, 17)); + + editor.setSelection(new Selection(9, 1, 9, 17)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(9), 'ParseHTML4String'); + assertSelection(editor, new Selection(9, 1, 9, 17)); + + editor.setSelection(new Selection(10, 1, 10, 11)); + executeAction(pascalCaseAction, editor); + assert.strictEqual(model.getLineContent(10), 'KebabCase'); + assertSelection(editor, new Selection(10, 1, 10, 10)); + } + ); }); suite('DeleteAllRightAction', () => { diff --git a/code/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts b/code/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts index 20ba9ff079b..de4198e7446 100644 --- a/code/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts +++ b/code/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts @@ -16,6 +16,7 @@ import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess' import { IKeyMods, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { status } from 'vs/base/browser/ui/aria/aria'; +import { TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; interface IEditorLineDecoration { readonly rangeHighlightId: string; @@ -141,7 +142,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; protected gotoLocation({ editor }: IQuickAccessTextEditorContext, options: { range: IRange; keyMods: IKeyMods; forceSideBySide?: boolean; preserveFocus?: boolean }): void { - editor.setSelection(options.range); + editor.setSelection(options.range, TextEditorSelectionSource.JUMP); editor.revealRangeInCenter(options.range, ScrollType.Smooth); if (!options.preserveFocus) { editor.focus(); diff --git a/code/src/vs/editor/contrib/rename/browser/rename.ts b/code/src/vs/editor/contrib/rename/browser/rename.ts index cb4f58e1073..8daef7eff27 100644 --- a/code/src/vs/editor/contrib/rename/browser/rename.ts +++ b/code/src/vs/editor/contrib/rename/browser/rename.ts @@ -38,7 +38,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CONTEXT_RENAME_INPUT_FOCUSED, CONTEXT_RENAME_INPUT_VISIBLE, RenameInputField, RenameInputFieldResult } from './renameInputField'; +import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameInputField, RenameInputFieldResult } from './renameInputField'; class RenameSkeleton { @@ -231,7 +231,6 @@ class RenameController implements IEditorContribution { const renameCandidatesCts = new CancellationTokenSource(cts2.token); const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model); - // TODO@ulugbekna: providers should get timeout token (use createTimeoutCancellation(x)) const newSymbolNameProvidersResults = newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, renameCandidatesCts.token)); trace(`requested new symbol names from ${newSymbolNamesProviders.length} providers`); @@ -252,6 +251,8 @@ class RenameController implements IEditorContribution { if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently this._reportTelemetry(newSymbolNamesProviders.length, model.getLanguageId(), inputFieldResult); } + // TODO@ulugbekna: remove before stable release + this._reportTelemetry(newSymbolNamesProviders.length, model.getLanguageId(), inputFieldResult, 'inDebugMode'); // no result, only hint to focus the editor or not if (typeof inputFieldResult === 'boolean') { @@ -339,7 +340,7 @@ class RenameController implements IEditorContribution { this._renameInputField.focusPreviousRenameSuggestion(); } - private _reportTelemetry(nRenameSuggestionProviders: number, languageId: string, inputFieldResult: boolean | RenameInputFieldResult) { + private _reportTelemetry(nRenameSuggestionProviders: number, languageId: string, inputFieldResult: boolean | RenameInputFieldResult, inDebugMode?: 'inDebugMode') { type RenameInvokedEvent = { kind: 'accepted' | 'cancelled'; @@ -347,10 +348,12 @@ class RenameController implements IEditorContribution { nRenameSuggestionProviders: number; /** provided only if kind = 'accepted' */ - source?: RenameInputFieldResult['source']; + source?: NewNameSource['k']; /** provided only if kind = 'accepted' */ nRenameSuggestions?: number; /** provided only if kind = 'accepted' */ + timeBeforeFirstInputFieldEdit?: number; + /** provided only if kind = 'accepted' */ wantsPreview?: boolean; }; @@ -364,6 +367,7 @@ class RenameController implements IEditorContribution { source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the new name came from the input field or rename suggestions.' }; nRenameSuggestions?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of rename suggestions user has got'; isMeasurement: true }; + timeBeforeFirstInputFieldEdit?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Milliseconds before user edits the input field for the first time'; isMeasurement: true }; wantsPreview?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If user wanted preview.'; isMeasurement: true }; }; @@ -378,12 +382,17 @@ class RenameController implements IEditorContribution { languageId, nRenameSuggestionProviders, - source: inputFieldResult.source, - nRenameSuggestions: inputFieldResult.nRenameSuggestions, + source: inputFieldResult.stats.source.k, + nRenameSuggestions: inputFieldResult.stats.nRenameSuggestions, + timeBeforeFirstInputFieldEdit: inputFieldResult.stats.timeBeforeFirstInputFieldEdit, wantsPreview: inputFieldResult.wantsPreview, }; - this._telemetryService.publicLog2('renameInvokedEvent', value); + if (inDebugMode) { + this._telemetryService.publicLog2('renameInvokedEventDebug', value); + } else { + this._telemetryService.publicLog2('renameInvokedEvent', value); + } } } @@ -521,12 +530,6 @@ registerAction2(class FocusPreviousRenameSuggestion extends Action2 { precondition: CONTEXT_RENAME_INPUT_VISIBLE, keybinding: [ { - when: CONTEXT_RENAME_INPUT_FOCUSED, - primary: KeyCode.Tab | KeyCode.Shift, - weight: KeybindingWeight.EditorContrib + 99, - }, - { - when: CONTEXT_RENAME_INPUT_FOCUSED.toNegated(), primary: KeyMod.Shift | KeyCode.Tab, secondary: [KeyCode.UpArrow], weight: KeybindingWeight.EditorContrib + 99, diff --git a/code/src/vs/editor/contrib/rename/browser/renameInputField.ts b/code/src/vs/editor/contrib/rename/browser/renameInputField.ts index 1b3728c30c6..af5f430254b 100644 --- a/code/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/code/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -3,18 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener, getClientArea, getDomNodePagePosition, getTotalHeight, getTotalWidth } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import * as arrays from 'vs/base/common/arrays'; -import { raceCancellation } from 'vs/base/common/async'; +import { DeferredPromise, raceCancellation } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Emitter } from 'vs/base/common/event'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType, isDefined } from 'vs/base/common/types'; import 'vs/css!./renameInputField'; +import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; @@ -27,12 +30,14 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; -import { defaultListStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { getListStyles } from 'vs/platform/theme/browser/defaultStyles'; import { editorWidgetBackground, inputBackground, inputBorder, inputForeground, + quickInputListFocusBackground, + quickInputListFocusForeground, widgetBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; @@ -47,29 +52,84 @@ const _sticky = false export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false, localize('renameInputVisible', "Whether the rename input widget is visible")); export const CONTEXT_RENAME_INPUT_FOCUSED = new RawContextKey('renameInputFocused', false, localize('renameInputFocused', "Whether the rename input widget is focused")); -export interface RenameInputFieldResult { +/** + * "Source" of the new name: + * - 'inputField' - user entered the new name + * - 'renameSuggestion' - user picked from rename suggestions + * - 'userEditedRenameSuggestion' - user _likely_ edited a rename suggestion ("likely" because when input started being edited, a rename suggestion had focus) + */ +export type NewNameSource = + | { k: 'inputField' } + | { k: 'renameSuggestion' } + | { k: 'userEditedRenameSuggestion' }; + +/** + * Various statistics regarding rename input field + */ +export type RenameInputFieldStats = { + nRenameSuggestions: number; + source: NewNameSource; + timeBeforeFirstInputFieldEdit: number | undefined; +}; + +export type RenameInputFieldResult = { + /** + * The new name to be used + */ newName: string; wantsPreview?: boolean; - source: 'inputField' | 'renameSuggestion'; - nRenameSuggestions: number; + stats: RenameInputFieldStats; +}; + +interface IRenameInputField { + /** + * @returns a `boolean` standing for `shouldFocusEditor`, if user didn't pick a new name, or a {@link RenameInputFieldResult} + */ + getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult[], cts: CancellationTokenSource): Promise; + + acceptInput(wantsPreview: boolean): void; + cancelInput(focusEditor: boolean, caller: string): void; + + focusNextRenameSuggestion(): void; + focusPreviousRenameSuggestion(): void; } -export class RenameInputField implements IContentWidget { +export class RenameInputField implements IRenameInputField, IContentWidget, IDisposable { + + // implement IContentWidget + readonly allowEditorOverflow: boolean = true; + + // UI state - private _position?: Position; private _domNode?: HTMLElement; - private _input?: HTMLInputElement; - private _candidatesView?: CandidatesView; + private _input: RenameInput; + private _renameCandidateListView?: RenameCandidateListView; private _label?: HTMLDivElement; - private _visible?: boolean; + private _nPxAvailableAbove?: number; private _nPxAvailableBelow?: number; + + // Model state + + private _position?: Position; + private _currentName?: string; + /** Is true if input field got changes when a rename candidate was focused; otherwise, false */ + private _isEditingRenameCandidate: boolean; + + private _visible?: boolean; + + /** must be reset at session start */ + private _beforeFirstInputFieldEditSW: StopWatch; + + /** + * Milliseconds before user edits the input field for the first time + * @remarks must be set once per session + */ + private _timeBeforeFirstInputFieldEdit: number | undefined; + private readonly _visibleContextKey: IContextKey; - private readonly _focusedContextKey: IContextKey; private readonly _disposables = new DisposableStore(); - readonly allowEditorOverflow: boolean = true; - constructor( private readonly _editor: ICodeEditor, private readonly _acceptKeybindings: [string, string], @@ -79,7 +139,13 @@ export class RenameInputField implements IContentWidget { @ILogService private readonly _logService: ILogService, ) { this._visibleContextKey = CONTEXT_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); - this._focusedContextKey = CONTEXT_RENAME_INPUT_FOCUSED.bindTo(contextKeyService); + + this._isEditingRenameCandidate = false; + + this._beforeFirstInputFieldEditSW = new StopWatch(); + + this._input = new RenameInput(); + this._disposables.add(this._input); this._editor.addContentWidget(this); @@ -106,19 +172,31 @@ export class RenameInputField implements IContentWidget { this._domNode = document.createElement('div'); this._domNode.className = 'monaco-editor rename-box'; - this._input = document.createElement('input'); - this._input.className = 'rename-input'; - this._input.type = 'text'; - this._input.setAttribute('aria-label', localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); - this._disposables.add(addDisposableListener(this._input, 'focus', () => { this._focusedContextKey.set(true); })); - this._disposables.add(addDisposableListener(this._input, 'blur', () => { this._focusedContextKey.reset(); })); - this._domNode.appendChild(this._input); + this._domNode.appendChild(this._input.domNode); - this._candidatesView = this._disposables.add( - new CandidatesView(this._domNode, { + this._renameCandidateListView = this._disposables.add( + new RenameCandidateListView(this._domNode, { fontInfo: this._editor.getOption(EditorOption.fontInfo), - onSelectionChange: () => this.acceptInput(false) // we don't allow preview with mouse click for now - })); + onFocusChange: (newSymbolName: string) => { + this._input.domNode.value = newSymbolName; + this._isEditingRenameCandidate = false; // @ulugbekna: reset + }, + onSelectionChange: () => { + this._isEditingRenameCandidate = false; // @ulugbekna: because user picked a rename suggestion + this.acceptInput(false); // we don't allow preview with mouse click for now + } + }) + ); + + this._disposables.add( + this._input.onDidChange(() => { + if (this._renameCandidateListView?.focusedCandidate !== undefined) { + this._isEditingRenameCandidate = true; + } + this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed(); + this._renameCandidateListView?.clearFocus(); + }) + ); this._label = document.createElement('div'); this._label.className = 'rename-label'; @@ -131,7 +209,7 @@ export class RenameInputField implements IContentWidget { } private _updateStyles(theme: IColorTheme): void { - if (!this._input || !this._domNode) { + if (!this._domNode) { return; } @@ -142,26 +220,23 @@ export class RenameInputField implements IContentWidget { this._domNode.style.border = widgetBorderColor ? `1px solid ${widgetBorderColor}` : ''; this._domNode.style.color = String(theme.getColor(inputForeground) ?? ''); - this._input.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); + this._input.domNode.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); // this._input.style.color = String(theme.getColor(inputForeground) ?? ''); const border = theme.getColor(inputBorder); - this._input.style.borderWidth = border ? '1px' : '0px'; - this._input.style.borderStyle = border ? 'solid' : 'none'; - this._input.style.borderColor = border?.toString() ?? 'none'; + this._input.domNode.style.borderWidth = border ? '1px' : '0px'; + this._input.domNode.style.borderStyle = border ? 'solid' : 'none'; + this._input.domNode.style.borderColor = border?.toString() ?? 'none'; } private _updateFont(): void { - if (!this._input || !this._label || !this._candidatesView) { + if (this._domNode === undefined) { return; } + assertType(this._label !== undefined, 'RenameInputField#_updateFont: _label must not be undefined given _domNode is defined'); - const fontInfo = this._editor.getOption(EditorOption.fontInfo); - this._input.style.fontFamily = fontInfo.fontFamily; - this._input.style.fontWeight = fontInfo.fontWeight; - this._input.style.fontSize = `${fontInfo.fontSize}px`; - - this._candidatesView.updateFont(fontInfo); + this._editor.applyFontInfo(this._input.domNode); + const fontInfo = this._editor.getOption(EditorOption.fontInfo); this._label.style.fontSize = `${this._computeLabelFontSize(fontInfo.fontSize)}px`; } @@ -180,8 +255,8 @@ export class RenameInputField implements IContentWidget { return null; } - const bodyBox = getClientArea(this.getDomNode().ownerDocument.body); - const editorBox = getDomNodePagePosition(this._editor.getDomNode()); + const bodyBox = dom.getClientArea(this.getDomNode().ownerDocument.body); + const editorBox = dom.getDomNodePagePosition(this._editor.getDomNode()); const cursorBoxTop = this._getTopForPosition(); @@ -189,7 +264,7 @@ export class RenameInputField implements IContentWidget { this._nPxAvailableBelow = bodyBox.height - this._nPxAvailableAbove; const lineHeight = this._editor.getOption(EditorOption.lineHeight); - const { totalHeight: candidateViewHeight } = CandidateView.getLayoutInfo({ lineHeight }); + const { totalHeight: candidateViewHeight } = RenameCandidateView.getLayoutInfo({ lineHeight }); const positionPreference = this._nPxAvailableBelow > candidateViewHeight * 6 /* approximate # of candidates to fit in (inclusive of rename input box & rename label) */ ? [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE] @@ -224,13 +299,13 @@ export class RenameInputField implements IContentWidget { return; } - assertType(this._candidatesView); + assertType(this._renameCandidateListView); assertType(this._nPxAvailableAbove !== undefined); assertType(this._nPxAvailableBelow !== undefined); - const inputBoxHeight = getTotalHeight(this._input!); + const inputBoxHeight = dom.getTotalHeight(this._input.domNode); - const labelHeight = getTotalHeight(this._label!); + const labelHeight = dom.getTotalHeight(this._label!); let totalHeightAvailable: number; if (position === ContentWidgetPositionPreference.BELOW) { @@ -239,9 +314,9 @@ export class RenameInputField implements IContentWidget { totalHeightAvailable = this._nPxAvailableAbove; } - this._candidatesView!.layout({ + this._renameCandidateListView!.layout({ height: totalHeightAvailable - labelHeight - inputBoxHeight, - width: getTotalWidth(this._input!), + width: dom.getTotalWidth(this._input.domNode), }); } @@ -260,93 +335,102 @@ export class RenameInputField implements IContentWidget { } focusNextRenameSuggestion() { - this._candidatesView?.focusNext(); + if (!this._renameCandidateListView?.focusNext()) { + this._input.domNode.value = this._currentName!; + } } - focusPreviousRenameSuggestion() { - if (!this._candidatesView?.focusPrevious()) { - this._input!.focus(); + focusPreviousRenameSuggestion() { // TODO@ulugbekna: this and focusNext should set the original name if no candidate is focused + if (!this._renameCandidateListView?.focusPrevious()) { + this._input.domNode.value = this._currentName!; } } - /** - * @returns a `boolean` standing for `shouldFocusEditor`, if user didn't pick a new name, or a {@link RenameInputFieldResult} - */ - getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult[], cts: CancellationTokenSource): Promise { + getInput(where: IRange, currentName: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult[], cts: CancellationTokenSource): Promise { + + this._isEditingRenameCandidate = false; this._domNode!.classList.toggle('preview', supportPreview); this._position = new Position(where.startLineNumber, where.startColumn); - this._input!.value = value; - this._input!.setAttribute('selectionStart', selectionStart.toString()); - this._input!.setAttribute('selectionEnd', selectionEnd.toString()); - this._input!.size = Math.max((where.endColumn - where.startColumn) * 1.1, 20); // determines width + this._currentName = currentName; + + this._input.domNode.value = currentName; + this._input.domNode.setAttribute('selectionStart', selectionStart.toString()); + this._input.domNode.setAttribute('selectionEnd', selectionEnd.toString()); + this._input.domNode.size = Math.max((where.endColumn - where.startColumn) * 1.1, 20); // determines width + + this._beforeFirstInputFieldEditSW.reset(); const disposeOnDone = new DisposableStore(); disposeOnDone.add(toDisposable(() => cts.dispose(true))); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call - this._updateRenameCandidates(candidates, value, cts.token); + this._updateRenameCandidates(candidates, currentName, cts.token); - return new Promise(resolve => { + const inputResult = new DeferredPromise(); - this._currentCancelInput = (focusEditor) => { - this._trace('invoking _currentCancelInput'); - this._currentAcceptInput = undefined; - this._currentCancelInput = undefined; - this._candidatesView?.clearCandidates(); - resolve(focusEditor); - return true; - }; - - this._currentAcceptInput = (wantsPreview) => { - this._trace('invoking _currentAcceptInput'); - assertType(this._input !== undefined); - assertType(this._candidatesView !== undefined); - - const nRenameSuggestions = this._candidatesView.nCandidates; - - let newName: string; - let source: 'inputField' | 'renameSuggestion'; - const focusedCandidate = this._candidatesView.focusedCandidate; - if (focusedCandidate !== undefined) { - this._trace('using new name from renameSuggestion'); - newName = focusedCandidate; - source = 'renameSuggestion'; - } else { - this._trace('using new name from inputField'); - newName = this._input.value; - source = 'inputField'; - } + inputResult.p.finally(() => { + disposeOnDone.dispose(); + this._hide(); + }); - if (newName === value || newName.trim().length === 0 /* is just whitespace */) { - this.cancelInput(true, '_currentAcceptInput (because newName === value || newName.trim().length === 0)'); - return; - } + this._currentCancelInput = (focusEditor) => { + this._trace('invoking _currentCancelInput'); + this._currentAcceptInput = undefined; + this._currentCancelInput = undefined; + this._renameCandidateListView?.clearCandidates(); + inputResult.complete(focusEditor); + return true; + }; - this._currentAcceptInput = undefined; - this._currentCancelInput = undefined; - this._candidatesView.clearCandidates(); + this._currentAcceptInput = (wantsPreview) => { + this._trace('invoking _currentAcceptInput'); + assertType(this._renameCandidateListView !== undefined); + + const nRenameSuggestions = this._renameCandidateListView.nCandidates; + + let newName: string; + let source: NewNameSource; + const focusedCandidate = this._renameCandidateListView.focusedCandidate; + if (focusedCandidate !== undefined) { + this._trace('using new name from renameSuggestion'); + newName = focusedCandidate; + source = { k: 'renameSuggestion' }; + } else { + this._trace('using new name from inputField'); + newName = this._input.domNode.value; + source = this._isEditingRenameCandidate ? { k: 'userEditedRenameSuggestion' } : { k: 'inputField' }; + } - resolve({ - newName, - wantsPreview: supportPreview && wantsPreview, + if (newName === currentName || newName.trim().length === 0 /* is just whitespace */) { + this.cancelInput(true, '_currentAcceptInput (because newName === value || newName.trim().length === 0)'); + return; + } + + this._currentAcceptInput = undefined; + this._currentCancelInput = undefined; + this._renameCandidateListView.clearCandidates(); + + inputResult.complete({ + newName, + wantsPreview: supportPreview && wantsPreview, + stats: { source, nRenameSuggestions, - }); - }; + timeBeforeFirstInputFieldEdit: this._timeBeforeFirstInputFieldEdit, + } + }); + }; - disposeOnDone.add(cts.token.onCancellationRequested(() => this.cancelInput(true, 'cts.token.onCancellationRequested'))); - if (!_sticky) { - disposeOnDone.add(this._editor.onDidBlurEditorWidget(() => this.cancelInput(!this._domNode?.ownerDocument.hasFocus(), 'editor.onDidBlurEditorWidget'))); - } + disposeOnDone.add(cts.token.onCancellationRequested(() => this.cancelInput(true, 'cts.token.onCancellationRequested'))); + if (!_sticky) { + disposeOnDone.add(this._editor.onDidBlurEditorWidget(() => this.cancelInput(!this._domNode?.ownerDocument.hasFocus(), 'editor.onDidBlurEditorWidget'))); + } - this._show(); + this._show(); - }).finally(() => { - disposeOnDone.dispose(); - this._hide(); - }); + return inputResult.p; } private _show(): void { @@ -356,11 +440,13 @@ export class RenameInputField implements IContentWidget { this._visibleContextKey.set(true); this._editor.layoutContentWidget(this); + // TODO@ulugbekna: could this be simply run in `afterRender`? setTimeout(() => { - this._input!.focus(); - this._input!.setSelectionRange( - parseInt(this._input!.getAttribute('selectionStart')!), - parseInt(this._input!.getAttribute('selectionEnd')!)); + this._input.domNode.focus(); + this._input.domNode.setSelectionRange( + parseInt(this._input!.domNode.getAttribute('selectionStart')!), + parseInt(this._input!.domNode.getAttribute('selectionEnd')!) + ); }, 100); } @@ -386,7 +472,7 @@ export class RenameInputField implements IContentWidget { const distinctNames = arrays.distinct(newNames, v => v.newSymbolName); trace(`distinct candidates - ${distinctNames.length} candidates.`); - const validDistinctNames = distinctNames.filter(({ newSymbolName }) => newSymbolName.trim().length > 0 && newSymbolName !== this._input?.value && newSymbolName !== currentName); + const validDistinctNames = distinctNames.filter(({ newSymbolName }) => newSymbolName.trim().length > 0 && newSymbolName !== this._input.domNode.value && newSymbolName !== currentName); trace(`valid distinct candidates - ${newNames.length} candidates.`); if (validDistinctNames.length < 1) { @@ -396,7 +482,7 @@ export class RenameInputField implements IContentWidget { // show the candidates trace('setting candidates'); - this._candidatesView!.setCandidates(validDistinctNames); + this._renameCandidateListView!.setCandidates(validDistinctNames); // ask editor to re-layout given that the widget is now of a different size after rendering rename candidates trace('asking editor to re-layout'); @@ -427,18 +513,27 @@ export class RenameInputField implements IContentWidget { } } -class CandidatesView { +class RenameCandidateListView { - private readonly _listWidget: List; + private readonly _onDidFocusChange = new Emitter(); + readonly onDidFocusChange = this._onDidFocusChange.event; + + private readonly _onDidSelectionChange = new Emitter(); + readonly onDidSelectionChange = this._onDidSelectionChange.event; + + /** Parent node of the list widget; needed to control # of list elements visible */ private readonly _listContainer: HTMLDivElement; + private readonly _listWidget: List; private _lineHeight: number; private _availableHeight: number; private _minimumWidth: number; + private _typicalHalfwidthCharacterWidth: number; private _disposables: DisposableStore; - constructor(parent: HTMLElement, opts: { fontInfo: FontInfo; onSelectionChange: () => void }) { + // FIXME@ulugbekna: rewrite using event emitters + constructor(parent: HTMLElement, opts: { fontInfo: FontInfo; onFocusChange: (newSymbolName: string) => void; onSelectionChange: () => void }) { this._disposables = new DisposableStore(); @@ -446,64 +541,41 @@ class CandidatesView { this._minimumWidth = 0; this._lineHeight = opts.fontInfo.lineHeight; + this._typicalHalfwidthCharacterWidth = opts.fontInfo.typicalHalfwidthCharacterWidth; this._listContainer = document.createElement('div'); - this._listContainer.style.fontFamily = opts.fontInfo.fontFamily; - this._listContainer.style.fontWeight = opts.fontInfo.fontWeight; - this._listContainer.style.fontSize = `${opts.fontInfo.fontSize}px`; parent.appendChild(this._listContainer); - const that = this; - - const virtualDelegate = new class implements IListVirtualDelegate { - getTemplateId(element: NewSymbolName): string { - return 'candidate'; - } - - getHeight(element: NewSymbolName): number { - return that._candidateViewHeight; - } - }; - - const renderer = new class implements IListRenderer { - readonly templateId = 'candidate'; - - renderTemplate(container: HTMLElement): CandidateView { - return new CandidateView(container, { lineHeight: that._lineHeight }); - } - - renderElement(candidate: NewSymbolName, index: number, templateData: CandidateView): void { - templateData.model = candidate; - } + this._listWidget = RenameCandidateListView._createListWidget(this._listContainer, this._candidateViewHeight, opts.fontInfo); - disposeTemplate(templateData: CandidateView): void { - templateData.dispose(); - } - }; + this._listWidget.onDidChangeFocus( + e => { + if (e.elements.length === 1) { + opts.onFocusChange(e.elements[0].newSymbolName); + } + }, + this._disposables + ); - this._listWidget = new List( - 'NewSymbolNameCandidates', - this._listContainer, - virtualDelegate, - [renderer], - { - keyboardSupport: false, // @ulugbekna: because we handle keyboard events through proper commands & keybinding service, see `rename.ts` - mouseSupport: true, - multipleSelectionSupport: false, - } + this._listWidget.onDidChangeSelection( + e => { + if (e.elements.length === 1) { + opts.onSelectionChange(); + } + }, + this._disposables ); - this._disposables.add(this._listWidget.onDidChangeSelection(e => { - if (e.elements.length > 0) { - opts.onSelectionChange(); - } - })); + this._disposables.add( + this._listWidget.onDidBlur(e => { // @ulugbekna: because list widget otherwise remembers last focused element and returns it as focused element + this._listWidget.setFocus([]); + }) + ); - this._disposables.add(this._listWidget.onDidBlur(e => { - this._listWidget.setFocus([]); + this._listWidget.style(getListStyles({ + listInactiveFocusForeground: quickInputListFocusForeground, + listInactiveFocusBackground: quickInputListFocusBackground, })); - - this._listWidget.style(defaultListStyles); } dispose() { @@ -560,27 +632,23 @@ class CandidatesView { return; } - public updateFont(fontInfo: FontInfo): void { - this._listContainer.style.fontFamily = fontInfo.fontFamily; - this._listContainer.style.fontWeight = fontInfo.fontWeight; - this._listContainer.style.fontSize = `${fontInfo.fontSize}px`; - - this._lineHeight = fontInfo.lineHeight; - - this._listWidget.rerender(); - } - - public focusNext(): void { + public focusNext(): boolean { if (this._listWidget.length === 0) { - return; + return false; } - if (this._listWidget.isDOMFocused()) { - this._listWidget.focusNext(); - } else { - this._listWidget.domFocus(); + const focusedIxs = this._listWidget.getFocus(); + if (focusedIxs.length === 0) { this._listWidget.focusFirst(); + return true; + } else { + if (focusedIxs[0] === this._listWidget.length - 1) { + this._listWidget.setFocus([]); + return false; + } else { + this._listWidget.focusNext(); + return true; + } } - this._listWidget.reveal(this._listWidget.getFocus()[0]); } /** @@ -590,17 +658,27 @@ class CandidatesView { if (this._listWidget.length === 0) { return false; } - this._listWidget.domFocus(); - const focusedIx = this._listWidget.getFocus()[0]; - if (focusedIx !== 0) { - this._listWidget.focusPrevious(); - this._listWidget.reveal(this._listWidget.getFocus()[0]); + const focusedIxs = this._listWidget.getFocus(); + if (focusedIxs.length === 0) { + this._listWidget.focusLast(); + return true; + } else { + if (focusedIxs[0] === 0) { + this._listWidget.setFocus([]); + return false; + } else { + this._listWidget.focusPrevious(); + return true; + } } - return focusedIx > 0; + } + + public clearFocus(): void { + this._listWidget.setFocus([]); } private get _candidateViewHeight(): number { - const { totalHeight } = CandidateView.getLayoutInfo({ lineHeight: this._lineHeight }); + const { totalHeight } = RenameCandidateView.getLayoutInfo({ lineHeight: this._lineHeight }); return totalHeight; } @@ -612,8 +690,7 @@ class CandidatesView { } private _pickListWidth(candidates: NewSymbolName[]): number { - const APPROXIMATE_CHAR_WIDTH = 7.2; // approximate # of pixes taken by a single character - const longestCandidateWidth = Math.ceil(Math.max(...candidates.map(c => c.newSymbolName.length)) * APPROXIMATE_CHAR_WIDTH); // TODO@ulugbekna: use editor#typicalCharacterWidth or something + const longestCandidateWidth = Math.ceil(Math.max(...candidates.map(c => c.newSymbolName.length)) * this._typicalHalfwidthCharacterWidth); const width = Math.max( this._minimumWidth, 4 /* padding */ + 16 /* sparkle icon */ + 5 /* margin-left */ + longestCandidateWidth + 10 /* (possibly visible) scrollbar width */ // TODO@ulugbekna: approximate calc - clean this up @@ -621,62 +698,127 @@ class CandidatesView { return width; } + private static _createListWidget(container: HTMLElement, candidateViewHeight: number, fontInfo: FontInfo) { + const virtualDelegate = new class implements IListVirtualDelegate { + getTemplateId(element: NewSymbolName): string { + return 'candidate'; + } + + getHeight(element: NewSymbolName): number { + return candidateViewHeight; + } + }; + + const renderer = new class implements IListRenderer { + readonly templateId = 'candidate'; + + renderTemplate(container: HTMLElement): RenameCandidateView { + return new RenameCandidateView(container, fontInfo); + } + + renderElement(candidate: NewSymbolName, index: number, templateData: RenameCandidateView): void { + templateData.populate(candidate); + } + + disposeTemplate(templateData: RenameCandidateView): void { + templateData.dispose(); + } + }; + + return new List( + 'NewSymbolNameCandidates', + container, + virtualDelegate, + [renderer], + { + keyboardSupport: false, // @ulugbekna: because we handle keyboard events through proper commands & keybinding service, see `rename.ts` + mouseSupport: true, + multipleSelectionSupport: false, + } + ); + } } -class CandidateView { +/** + * @remarks lazily creates the DOM node + */ +class RenameInput implements IDisposable { + + private _domNode: HTMLInputElement | undefined; - // TODO@ulugbekna: accessibility + private readonly _onDidChange = new Emitter(); + public readonly onDidChange = this._onDidChange.event; + + private _disposables = new DisposableStore(); + + get domNode() { + if (!this._domNode) { + this._domNode = document.createElement('input'); + this._domNode.className = 'rename-input'; + this._domNode.type = 'text'; + this._domNode.setAttribute('aria-label', localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); + this._disposables.add(dom.addDisposableListener(this._domNode, 'input', () => this._onDidChange.fire())); + } + return this._domNode; + } + + dispose(): void { + this._onDidChange.dispose(); + this._disposables.dispose(); + } +} + +class RenameCandidateView { private static _PADDING: number = 2; - public readonly domNode: HTMLElement; + private readonly _domNode: HTMLElement; private readonly _icon: HTMLElement; private readonly _label: HTMLElement; - constructor(parent: HTMLElement, { lineHeight }: { lineHeight: number }) { + constructor(parent: HTMLElement, fontInfo: FontInfo) { + + this._domNode = document.createElement('div'); + this._domNode.style.display = `flex`; + this._domNode.style.columnGap = `5px`; + this._domNode.style.alignItems = `center`; + this._domNode.style.height = `${fontInfo.lineHeight}px`; + this._domNode.style.padding = `${RenameCandidateView._PADDING}px`; - this.domNode = document.createElement('div'); - this.domNode.style.display = `flex`; - this.domNode.style.alignItems = `center`; - this.domNode.style.height = `${lineHeight}px`; - this.domNode.style.padding = `${CandidateView._PADDING}px`; + // @ulugbekna: needed to keep space when the `icon.style.display` is set to `none` + const iconContainer = document.createElement('div'); + iconContainer.style.display = `flex`; + iconContainer.style.alignItems = `center`; + iconContainer.style.width = iconContainer.style.height = `${fontInfo.lineHeight * 0.8}px`; + this._domNode.appendChild(iconContainer); - this._icon = document.createElement('div'); - this._icon.style.display = `flex`; - this._icon.style.alignItems = `center`; - this._icon.style.width = this._icon.style.height = `${lineHeight * 0.8}px`; - this.domNode.appendChild(this._icon); + this._icon = renderIcon(Codicon.sparkle); + this._icon.style.display = `none`; + iconContainer.appendChild(this._icon); this._label = document.createElement('div'); - this._icon.style.display = `flex`; - this._icon.style.alignItems = `center`; - this._label.style.marginLeft = '5px'; - this.domNode.appendChild(this._label); + applyFontInfo(this._label, fontInfo); + this._domNode.appendChild(this._label); - parent.appendChild(this.domNode); + parent.appendChild(this._domNode); } - public set model(value: NewSymbolName) { - - // @ulugbekna: a hack to always include sparkle for now - const alwaysIncludeSparkle = true; + public populate(value: NewSymbolName) { + this._updateIcon(value); + this._updateLabel(value); + } - // update icon - if (alwaysIncludeSparkle || value.tags?.includes(NewSymbolNameTag.AIGenerated)) { - if (this._icon.children.length === 0) { - this._icon.appendChild(renderIcon(Codicon.sparkle)); - } - } else { - if (this._icon.children.length === 1) { - this._icon.removeChild(this._icon.children[0]); - } - } + private _updateIcon(value: NewSymbolName) { + const isAIGenerated = !!value.tags?.includes(NewSymbolNameTag.AIGenerated); + this._icon.style.display = isAIGenerated ? 'inherit' : 'none'; + } + private _updateLabel(value: NewSymbolName) { this._label.innerText = value.newSymbolName; } public static getLayoutInfo({ lineHeight }: { lineHeight: number }): { totalHeight: number } { - const totalHeight = lineHeight + CandidateView._PADDING * 2 /* top & bottom padding */; + const totalHeight = lineHeight + RenameCandidateView._PADDING * 2 /* top & bottom padding */; return { totalHeight }; } diff --git a/code/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts b/code/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts new file mode 100644 index 00000000000..f3296062d81 --- /dev/null +++ b/code/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts @@ -0,0 +1,208 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorOption, IEditorMinimapOptions } from 'vs/editor/common/config/editorOptions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IModelDeltaDecoration, MinimapPosition, MinimapSectionHeaderStyle, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +import { FindSectionHeaderOptions, SectionHeader } from 'vs/editor/common/services/findSectionHeaders'; + +export class SectionHeaderDetector extends Disposable implements IEditorContribution { + + public static readonly ID: string = 'editor.sectionHeaderDetector'; + + private options: FindSectionHeaderOptions | undefined; + private decorations = this.editor.createDecorationsCollection(); + private computeSectionHeaders: RunOnceScheduler; + private computePromise: CancelablePromise | null; + private currentOccurrences: { [decorationId: string]: SectionHeaderOccurrence }; + + constructor( + private readonly editor: ICodeEditor, + @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, + @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, + ) { + super(); + + this.options = this.createOptions(editor.getOption(EditorOption.minimap)); + this.computePromise = null; + this.currentOccurrences = {}; + + this._register(editor.onDidChangeModel((e) => { + this.currentOccurrences = {}; + this.options = this.createOptions(editor.getOption(EditorOption.minimap)); + this.stop(); + this.computeSectionHeaders.schedule(0); + })); + + this._register(editor.onDidChangeModelLanguage((e) => { + this.currentOccurrences = {}; + this.options = this.createOptions(editor.getOption(EditorOption.minimap)); + this.stop(); + this.computeSectionHeaders.schedule(0); + })); + + this._register(languageConfigurationService.onDidChange((e) => { + const editorLanguageId = this.editor.getModel()?.getLanguageId(); + if (editorLanguageId && e.affects(editorLanguageId)) { + this.currentOccurrences = {}; + this.options = this.createOptions(editor.getOption(EditorOption.minimap)); + this.stop(); + this.computeSectionHeaders.schedule(0); + } + })); + + this._register(editor.onDidChangeConfiguration(e => { + if (this.options && !e.hasChanged(EditorOption.minimap)) { + return; + } + + this.options = this.createOptions(editor.getOption(EditorOption.minimap)); + + // Remove any links (for the getting disabled case) + this.updateDecorations([]); + + // Stop any computation (for the getting disabled case) + this.stop(); + + // Start computing (for the getting enabled case) + this.computeSectionHeaders.schedule(0); + })); + + this._register(this.editor.onDidChangeModelContent(e => { + this.computeSectionHeaders.schedule(); + })); + + this.computeSectionHeaders = this._register(new RunOnceScheduler(() => { + this.findSectionHeaders(); + }, 250)); + + this.computeSectionHeaders.schedule(0); + } + + private createOptions(minimap: Readonly>): FindSectionHeaderOptions | undefined { + if (!minimap || !this.editor.hasModel()) { + return undefined; + } + + const languageId = this.editor.getModel().getLanguageId(); + if (!languageId) { + return undefined; + } + + const commentsConfiguration = this.languageConfigurationService.getLanguageConfiguration(languageId).comments; + const foldingRules = this.languageConfigurationService.getLanguageConfiguration(languageId).foldingRules; + + if (!commentsConfiguration && !foldingRules?.markers) { + return undefined; + } + + return { + foldingRules, + findMarkSectionHeaders: minimap.showMarkSectionHeaders, + findRegionSectionHeaders: minimap.showRegionSectionHeaders, + }; + } + + private findSectionHeaders() { + if (!this.editor.hasModel() + || (!this.options?.findMarkSectionHeaders && !this.options?.findRegionSectionHeaders)) { + return; + } + + const model = this.editor.getModel(); + if (model.isDisposed() || model.isTooLargeForSyncing()) { + return; + } + + const modelVersionId = model.getVersionId(); + this.editorWorkerService.findSectionHeaders(model.uri, this.options) + .then((sectionHeaders) => { + if (model.isDisposed() || model.getVersionId() !== modelVersionId) { + // model changed in the meantime + return; + } + this.updateDecorations(sectionHeaders); + }); + } + + private updateDecorations(sectionHeaders: SectionHeader[]): void { + + const model = this.editor.getModel(); + if (model) { + // Remove all section headers that should be in comments and are not in comments + sectionHeaders = sectionHeaders.filter((sectionHeader) => { + if (!sectionHeader.shouldBeInComments) { + return true; + } + const validRange = model.validateRange(sectionHeader.range); + const tokens = model.tokenization.getLineTokens(validRange.startLineNumber); + const idx = tokens.findTokenIndexAtOffset(validRange.startColumn - 1); + const tokenType = tokens.getStandardTokenType(idx); + const languageId = tokens.getLanguageId(idx); + return (languageId === model.getLanguageId() && tokenType === StandardTokenType.Comment); + }); + } + + const oldDecorations = Object.values(this.currentOccurrences).map(occurrence => occurrence.decorationId); + const newDecorations = sectionHeaders.map(sectionHeader => decoration(sectionHeader)); + + this.editor.changeDecorations((changeAccessor) => { + const decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations); + + this.currentOccurrences = {}; + for (let i = 0, len = decorations.length; i < len; i++) { + const occurrence = { sectionHeader: sectionHeaders[i], decorationId: decorations[i] }; + this.currentOccurrences[occurrence.decorationId] = occurrence; + } + }); + } + + private stop(): void { + this.computeSectionHeaders.cancel(); + if (this.computePromise) { + this.computePromise.cancel(); + this.computePromise = null; + } + } + + public override dispose(): void { + super.dispose(); + this.stop(); + this.decorations.clear(); + } + +} + +interface SectionHeaderOccurrence { + readonly sectionHeader: SectionHeader; + readonly decorationId: string; +} + +function decoration(sectionHeader: SectionHeader): IModelDeltaDecoration { + return { + range: sectionHeader.range, + options: ModelDecorationOptions.createDynamic({ + description: 'section-header', + stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingAfter, + collapseOnReplaceEdit: true, + minimap: { + color: undefined, + position: MinimapPosition.Inline, + sectionHeaderStyle: sectionHeader.hasSeparatorLine ? MinimapSectionHeaderStyle.Underlined : MinimapSectionHeaderStyle.Normal, + sectionHeaderText: sectionHeader.text, + }, + }) + }; +} + +registerEditorContribution(SectionHeaderDetector.ID, SectionHeaderDetector, EditorContributionInstantiation.AfterFirstRender); diff --git a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts index f1f50f9ced7..231052024c6 100644 --- a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts +++ b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts @@ -3,23 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; import { FoldingController, RangesLimitReporter } from 'vs/editor/contrib/folding/browser/folding'; -import { ITextModel } from 'vs/editor/common/model'; import { SyntaxRangeProvider } from 'vs/editor/contrib/folding/browser/syntaxRangeProvider'; import { IndentRangeProvider } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { FoldingRegions } from 'vs/editor/contrib/folding/browser/foldingRanges'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { StickyElement, StickyModel, StickyRange } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollElement'; import { Iterable } from 'vs/base/common/iterator'; -import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; enum ModelProvider { OUTLINE_MODEL = 'outlineModel', @@ -33,16 +32,14 @@ enum Status { CANCELED } -export interface IStickyModelProvider { +export interface IStickyModelProvider extends IDisposable { /** * Method which updates the sticky model - * @param textModel text-model of the editor - * @param textModelVersionId text-model version ID * @param token cancellation token * @returns the sticky model */ - update(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise; + update(token: CancellationToken): Promise; } export class StickyModelProvider extends Disposable implements IStickyModelProvider { @@ -53,33 +50,33 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi private readonly _updateOperation: DisposableStore = this._register(new DisposableStore()); constructor( - private readonly _editor: ICodeEditor, - @ILanguageConfigurationService readonly _languageConfigurationService: ILanguageConfigurationService, + private readonly _editor: IActiveCodeEditor, + onProviderUpdate: () => void, + @IInstantiationService readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService readonly _languageFeaturesService: ILanguageFeaturesService, - defaultModel: string ) { super(); - const stickyModelFromCandidateOutlineProvider = new StickyModelFromCandidateOutlineProvider(_languageFeaturesService); - const stickyModelFromSyntaxFoldingProvider = new StickyModelFromCandidateSyntaxFoldingProvider(this._editor, _languageFeaturesService); - const stickyModelFromIndentationFoldingProvider = new StickyModelFromCandidateIndentationFoldingProvider(this._editor, _languageConfigurationService); - - switch (defaultModel) { + switch (this._editor.getOption(EditorOption.stickyScroll).defaultModel) { case ModelProvider.OUTLINE_MODEL: - this._modelProviders.push(stickyModelFromCandidateOutlineProvider); - this._modelProviders.push(stickyModelFromSyntaxFoldingProvider); - this._modelProviders.push(stickyModelFromIndentationFoldingProvider); - break; + this._modelProviders.push(new StickyModelFromCandidateOutlineProvider(this._editor, _languageFeaturesService)); + // fall through case ModelProvider.FOLDING_PROVIDER_MODEL: - this._modelProviders.push(stickyModelFromSyntaxFoldingProvider); - this._modelProviders.push(stickyModelFromIndentationFoldingProvider); - break; + this._modelProviders.push(new StickyModelFromCandidateSyntaxFoldingProvider(this._editor, onProviderUpdate, _languageFeaturesService)); + // fall through case ModelProvider.INDENTATION_MODEL: - this._modelProviders.push(stickyModelFromIndentationFoldingProvider); + this._modelProviders.push(new StickyModelFromCandidateIndentationFoldingProvider(this._editor, _languageConfigurationService)); break; } } + public override dispose(): void { + this._modelProviders.forEach(provider => provider.dispose()); + this._updateOperation.clear(); + this._cancelModelPromise(); + super.dispose(); + } + private _cancelModelPromise(): void { if (this._modelPromise) { this._modelPromise.cancel(); @@ -87,7 +84,7 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi } } - public async update(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise { + public async update(token: CancellationToken): Promise { this._updateOperation.clear(); this._updateOperation.add({ @@ -101,11 +98,7 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi return await this._updateScheduler.trigger(async () => { for (const modelProvider of this._modelProviders) { - const { statusPromise, modelPromise } = modelProvider.computeStickyModel( - textModel, - textModelVersionId, - token - ); + const { statusPromise, modelPromise } = modelProvider.computeStickyModel(token); this._modelPromise = modelPromise; const status = await statusPromise; if (this._modelPromise !== modelPromise) { @@ -127,26 +120,24 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi } } -interface IStickyModelCandidateProvider { +interface IStickyModelCandidateProvider extends IDisposable { get stickyModel(): StickyModel | null; - get provider(): LanguageFeatureRegistry | null; - /** * Method which computes the sticky model and returns a status to signal whether the sticky model has been successfully found - * @param textmodel text-model of the editor - * @param modelVersionId version ID of the text-model * @param token cancellation token * @returns a promise of a status indicating whether the sticky model has been successfully found as well as the model promise */ - computeStickyModel(textmodel: ITextModel, modelVersionId: number, token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null }; + computeStickyModel(token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null }; } -abstract class StickyModelCandidateProvider implements IStickyModelCandidateProvider { +abstract class StickyModelCandidateProvider extends Disposable implements IStickyModelCandidateProvider { protected _stickyModel: StickyModel | null = null; - constructor() { } + constructor(protected readonly _editor: IActiveCodeEditor) { + super(); + } get stickyModel(): StickyModel | null { return this._stickyModel; @@ -157,13 +148,11 @@ abstract class StickyModelCandidateProvider implements IStickyModelCandidateP return Status.INVALID; } - public abstract get provider(): LanguageFeatureRegistry | null; - - public computeStickyModel(textModel: ITextModel, modelVersionId: number, token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null } { - if (token.isCancellationRequested || !this.isProviderValid(textModel)) { + public computeStickyModel(token: CancellationToken): { statusPromise: Promise | Status; modelPromise: CancelablePromise | null } { + if (token.isCancellationRequested || !this.isProviderValid()) { return { statusPromise: this._invalid(), modelPromise: null }; } - const providerModelPromise = createCancelablePromise(token => this.createModelFromProvider(textModel, modelVersionId, token)); + const providerModelPromise = createCancelablePromise(token => this.createModelFromProvider(token)); return { statusPromise: providerModelPromise.then(providerModel => { @@ -174,7 +163,7 @@ abstract class StickyModelCandidateProvider implements IStickyModelCandidateP if (token.isCancellationRequested) { return Status.CANCELED; } - this._stickyModel = this.createStickyModel(textModel, modelVersionId, token, providerModel); + this._stickyModel = this.createStickyModel(token, providerModel); return Status.VALID; }).then(undefined, (err) => { onUnexpectedError(err); @@ -190,57 +179,49 @@ abstract class StickyModelCandidateProvider implements IStickyModelCandidateP * @param model model returned by the provider * @returns boolean indicating whether the model is valid */ - protected isModelValid(model: any): boolean { + protected isModelValid(model: T): boolean { return true; } /** * Method which checks whether the provider is valid before applying it to find the provider model. * This method by default returns true. - * @param textModel text-model of the editor * @returns boolean indicating whether the provider is valid */ - protected isProviderValid(textModel: ITextModel): boolean { + protected isProviderValid(): boolean { return true; } /** * Abstract method which creates the model from the provider and returns the provider model - * @param textModel text-model of the editor - * @param textModelVersionId text-model version ID * @param token cancellation token * @returns the model returned by the provider */ - protected abstract createModelFromProvider(textModel: ITextModel, textModelVersionId: number, token: CancellationToken): Promise; + protected abstract createModelFromProvider(token: CancellationToken): Promise; /** * Abstract method which computes the sticky model from the model returned by the provider and returns the sticky model - * @param textModel text-model of the editor - * @param textModelVersionId text-model version ID * @param token cancellation token * @param model model returned by the provider * @returns the sticky model */ - protected abstract createStickyModel(textModel: ITextModel, textModelVersionId: number, token: CancellationToken, model: T): StickyModel; + protected abstract createStickyModel(token: CancellationToken, model: T): StickyModel; } class StickyModelFromCandidateOutlineProvider extends StickyModelCandidateProvider { - constructor(@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) { - super(); + constructor(_editor: IActiveCodeEditor, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) { + super(_editor); } - public get provider(): LanguageFeatureRegistry | null { - return this._languageFeaturesService.documentSymbolProvider; + protected createModelFromProvider(token: CancellationToken): Promise { + return OutlineModel.create(this._languageFeaturesService.documentSymbolProvider, this._editor.getModel(), token); } - protected createModelFromProvider(textModel: ITextModel, modelVersionId: number, token: CancellationToken): Promise { - return OutlineModel.create(this._languageFeaturesService.documentSymbolProvider, textModel, token); - } - - protected createStickyModel(textModel: TextModel, modelVersionId: number, token: CancellationToken, model: OutlineModel): StickyModel { + protected createStickyModel(token: CancellationToken, model: OutlineModel): StickyModel { const { stickyOutlineElement, providerID } = this._stickyModelFromOutlineModel(model, this._stickyModel?.outlineProviderId); - return new StickyModel(textModel.uri, modelVersionId, stickyOutlineElement, providerID); + const textModel = this._editor.getModel(); + return new StickyModel(textModel.uri, textModel.getVersionId(), stickyOutlineElement, providerID); } protected override isModelValid(model: OutlineModel): boolean { @@ -334,14 +315,15 @@ abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandid protected _foldingLimitReporter: RangesLimitReporter; - constructor(editor: ICodeEditor) { - super(); + constructor(editor: IActiveCodeEditor) { + super(editor); this._foldingLimitReporter = new RangesLimitReporter(editor); } - protected createStickyModel(textModel: ITextModel, modelVersionId: number, token: CancellationToken, model: FoldingRegions): StickyModel { + protected createStickyModel(token: CancellationToken, model: FoldingRegions): StickyModel { const foldingElement = this._fromFoldingRegions(model); - return new StickyModel(textModel.uri, modelVersionId, foldingElement, undefined); + const textModel = this._editor.getModel(); + return new StickyModel(textModel.uri, textModel.getVersionId(), foldingElement, undefined); } protected override isModelValid(model: FoldingRegions): boolean { @@ -387,41 +369,41 @@ abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandid class StickyModelFromCandidateIndentationFoldingProvider extends StickyModelFromCandidateFoldingProvider { + private readonly provider: IndentRangeProvider; + constructor( - editor: ICodeEditor, + editor: IActiveCodeEditor, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService) { super(editor); - } - public get provider(): LanguageFeatureRegistry | null { - return null; + this.provider = this._register(new IndentRangeProvider(editor.getModel(), this._languageConfigurationService, this._foldingLimitReporter)); } - protected createModelFromProvider(textModel: TextModel, modelVersionId: number, token: CancellationToken): Promise { - const provider = new IndentRangeProvider(textModel, this._languageConfigurationService, this._foldingLimitReporter); - return provider.compute(token); + protected override async createModelFromProvider(token: CancellationToken): Promise { + return this.provider.compute(token); } } class StickyModelFromCandidateSyntaxFoldingProvider extends StickyModelFromCandidateFoldingProvider { - constructor(editor: ICodeEditor, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) { - super(editor); - } + private readonly provider: SyntaxRangeProvider | undefined; - public get provider(): LanguageFeatureRegistry | null { - return this._languageFeaturesService.foldingRangeProvider; + constructor(editor: IActiveCodeEditor, + onProviderUpdate: () => void, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService + ) { + super(editor); + const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, editor.getModel()); + if (selectedProviders.length > 0) { + this.provider = this._register(new SyntaxRangeProvider(editor.getModel(), selectedProviders, onProviderUpdate, this._foldingLimitReporter, undefined)); + } } - protected override isProviderValid(textModel: TextModel): boolean { - const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, textModel); - return selectedProviders.length > 0; + protected override isProviderValid(): boolean { + return this.provider !== undefined; } - protected createModelFromProvider(textModel: TextModel, modelVersionId: number, token: CancellationToken): Promise { - const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, textModel); - const provider = new SyntaxRangeProvider(textModel, selectedProviders, () => this.createModelFromProvider(textModel, modelVersionId, token), this._foldingLimitReporter, undefined); - return provider.compute(token); + protected override async createModelFromProvider(token: CancellationToken): Promise { + return this.provider?.compute(token) ?? null; } } diff --git a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 3388380f97c..705ef76489e 100644 --- a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CancellationToken, CancellationTokenSource, } from 'vs/base/common/cancellation'; -import { EditorOption, IEditorStickyScrollOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Range } from 'vs/editor/common/core/range'; import { binarySearch } from 'vs/base/common/arrays'; @@ -45,7 +45,6 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi private readonly _updateSoon: RunOnceScheduler; private readonly _sessionStore: DisposableStore; - private _options: Readonly> | null = null; private _model: StickyModel | null = null; private _cts: CancellationTokenSource | null = null; private _stickyModelProvider: IStickyModelProvider | null = null; @@ -69,26 +68,18 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi } private readConfiguration() { - - this._stickyModelProvider = null; this._sessionStore.clear(); - this._options = this._editor.getOption(EditorOption.stickyScroll); - if (!this._options.enabled) { + const options = this._editor.getOption(EditorOption.stickyScroll); + if (!options.enabled) { return; } - this._stickyModelProvider = this._sessionStore.add(new StickyModelProvider( - this._editor, - this._languageConfigurationService, - this._languageFeaturesService, - this._options.defaultModel - )); - 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.updateStickyModelProvider(); this._onDidChangeStickyScroll.fire(); this.update(); @@ -96,6 +87,11 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi 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())); + this._sessionStore.add(toDisposable(() => { + this._stickyModelProvider?.dispose(); + this._stickyModelProvider = null; + })); + this.updateStickyModelProvider(); this.update(); } @@ -103,6 +99,21 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi return this._model?.version; } + private updateStickyModelProvider() { + this._stickyModelProvider?.dispose(); + this._stickyModelProvider = null; + + const editor = this._editor; + if (editor.hasModel()) { + this._stickyModelProvider = new StickyModelProvider( + editor, + () => this._updateSoon.schedule(), + this._languageConfigurationService, + this._languageFeaturesService + ); + } + } + public async update(): Promise { this._cts?.dispose(true); this._cts = new CancellationTokenSource(); @@ -116,11 +127,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi this._model = null; return; } - - const textModel = this._editor.getModel(); - const modelVersionId = textModel.getVersionId(); - - const model = await this._stickyModelProvider.update(textModel, modelVersionId, token); + const model = await this._stickyModelProvider.update(token); if (token.isCancellationRequested) { // the computation was canceled, so do not overwrite the model return; diff --git a/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index 8b9790bce21..cae43742902 100644 --- a/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -310,6 +310,11 @@ class WordHighlighter { this._onPositionChanged(e); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { + if (this.occurrencesHighlight === 'off') { + // Early exit if nothing needs to be done + return; + } + if (!this.workerRequest) { this._run(); } diff --git a/code/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts b/code/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts index 6d78b460965..388d42021d2 100644 --- a/code/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts +++ b/code/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts @@ -45,7 +45,7 @@ export abstract class MoveWordCommand extends EditorCommand { if (!editor.hasModel()) { return; } - const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators)); + const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators), editor.getOption(EditorOption.wordSegmenterLocales)); const model = editor.getModel(); const selections = editor.getSelections(); @@ -187,8 +187,8 @@ export class CursorWordAccessibilityLeft extends WordLeftCommand { }); } - protected override _move(_: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); } } @@ -202,8 +202,8 @@ export class CursorWordAccessibilityLeftSelect extends WordLeftCommand { }); } - protected override _move(_: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); } } @@ -295,8 +295,8 @@ export class CursorWordAccessibilityRight extends WordRightCommand { }); } - protected override _move(_: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); } } @@ -310,8 +310,8 @@ export class CursorWordAccessibilityRightSelect extends WordRightCommand { }); } - protected override _move(_: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); } } @@ -336,7 +336,7 @@ export abstract class DeleteWordCommand extends EditorCommand { if (!editor.hasModel()) { return; } - const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators)); + const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators), editor.getOption(EditorOption.wordSegmenterLocales)); const model = editor.getModel(); const selections = editor.getSelections(); const autoClosingBrackets = editor.getOption(EditorOption.autoClosingBrackets); @@ -354,7 +354,7 @@ export abstract class DeleteWordCommand extends EditorCommand { autoClosingBrackets, autoClosingQuotes, autoClosingPairs, - autoClosedCharacters: viewModel.getCursorAutoClosedCharacters() + autoClosedCharacters: viewModel.getCursorAutoClosedCharacters(), }, this._wordNavigationType); return new ReplaceCommand(deleteRange, ''); }); @@ -482,7 +482,7 @@ export class DeleteInsideWord extends EditorAction { if (!editor.hasModel()) { return; } - const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators)); + const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators), editor.getOption(EditorOption.wordSegmenterLocales)); const model = editor.getModel(); const selections = editor.getSelections(); diff --git a/code/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts b/code/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts index 399d020170e..a06bf07200a 100644 --- a/code/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts +++ b/code/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts @@ -179,6 +179,44 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); + test('cursorWordLeft - Recognize words', () => { + const EXPECTED = [ + '|/* |これ|は|テスト|です |/*', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => cursorWordLeft(ed, true), + ed => ed.getPosition()!, + ed => ed.getPosition()!.equals(new Position(1, 1)), + { + wordSegmenterLocales: 'ja' + } + ); + const actual = serializePipePositions(text, actualStops); + assert.deepStrictEqual(actual, EXPECTED); + }); + + test('cursorWordLeft - Does not recognize words', () => { + const EXPECTED = [ + '|/* |これはテストです |/*', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => cursorWordLeft(ed, true), + ed => ed.getPosition()!, + ed => ed.getPosition()!.equals(new Position(1, 1)), + { + wordSegmenterLocales: '' + } + ); + const actual = serializePipePositions(text, actualStops); + assert.deepStrictEqual(actual, EXPECTED); + }); + test('cursorWordLeftSelect - issue #74369: cursorWordLeft and cursorWordLeftSelect do not behave consistently', () => { const EXPECTED = [ '|this.|is.|a.|test', @@ -327,6 +365,44 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); + test('cursorWordRight - Recognize words', () => { + const EXPECTED = [ + '/*| これ|は|テスト|です|/*|', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => cursorWordRight(ed), + ed => ed.getPosition()!, + ed => ed.getPosition()!.equals(new Position(1, 14)), + { + wordSegmenterLocales: 'ja' + } + ); + const actual = serializePipePositions(text, actualStops); + assert.deepStrictEqual(actual, EXPECTED); + }); + + test('cursorWordRight - Does not recognize words', () => { + const EXPECTED = [ + '/*| これはテストです|/*|', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => cursorWordRight(ed), + ed => ed.getPosition()!, + ed => ed.getPosition()!.equals(new Position(1, 14)), + { + wordSegmenterLocales: '' + } + ); + const actual = serializePipePositions(text, actualStops); + assert.deepStrictEqual(actual, EXPECTED); + }); + test('moveWordEndRight', () => { const EXPECTED = [ ' /*| Just| some| more| text| a|+=| 3| +5|-3| +| 7| */| |', diff --git a/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index 2ef43ea18a4..57d3dd3819b 100644 --- a/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -148,7 +148,7 @@ class Arrow { dom.removeCSSRulesContainingSelector(this._ruleName); dom.createCSSRule( `.monaco-editor ${this._ruleName}`, - `border-style: solid; border-color: transparent; border-bottom-color: ${this._color}; border-width: ${this._height}px; bottom: -${this._height}px; margin-left: -${this._height}px; ` + `border-style: solid; border-color: transparent; border-bottom-color: ${this._color}; border-width: ${this._height}px; bottom: -${this._height}px !important; margin-left: -${this._height}px; ` ); } diff --git a/code/src/vs/editor/editor.all.ts b/code/src/vs/editor/editor.all.ts index 37ed9f9f4fd..a84a6bdcb3f 100644 --- a/code/src/vs/editor/editor.all.ts +++ b/code/src/vs/editor/editor.all.ts @@ -44,6 +44,7 @@ import 'vs/editor/contrib/multicursor/browser/multicursor'; import 'vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution'; import 'vs/editor/contrib/parameterHints/browser/parameterHints'; import 'vs/editor/contrib/rename/browser/rename'; +import 'vs/editor/contrib/sectionHeaders/browser/sectionHeaders'; import 'vs/editor/contrib/semanticTokens/browser/documentSemanticTokens'; import 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import 'vs/editor/contrib/smartSelect/browser/smartSelect'; diff --git a/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 0c94d2824e9..709cb064db0 100644 --- a/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -49,6 +49,7 @@ class EditorScopedQuickInputService extends QuickInputService { _serviceBrand: undefined, get mainContainer() { return widget.getDomNode(); }, getContainer() { return widget.getDomNode(); }, + whenContainerStylesLoaded() { return undefined; }, get containers() { return [widget.getDomNode()]; }, get activeContainer() { return widget.getDomNode(); }, get mainContainerDimension() { return editor.getLayoutInfo(); }, @@ -58,7 +59,6 @@ class EditorScopedQuickInputService extends QuickInputService { get onDidLayoutContainer() { return Event.map(editor.onDidLayoutChange, dimension => ({ container: widget.getDomNode(), dimension })); }, get onDidChangeActiveContainer() { return Event.None; }, get onDidAddContainer() { return Event.None; }, - get whenActiveContainerStylesLoaded() { return Promise.resolve(); }, get mainContainerOffset() { return { top: 0, quickPickTop: 0 }; }, get activeContainerOffset() { return { top: 0, quickPickTop: 0 }; }, focus: () => editor.focus() diff --git a/code/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/code/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 3e903d5bab9..05305694e57 100644 --- a/code/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/code/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -39,7 +39,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { mainWindow } from 'vs/base/browser/window'; -import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; /** diff --git a/code/src/vs/editor/standalone/browser/standaloneEditor.ts b/code/src/vs/editor/standalone/browser/standaloneEditor.ts index 0fa038765af..d1a70eba3d2 100644 --- a/code/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/code/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -550,6 +550,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { EndOfLinePreference: standaloneEnums.EndOfLinePreference, EndOfLineSequence: standaloneEnums.EndOfLineSequence, MinimapPosition: standaloneEnums.MinimapPosition, + MinimapSectionHeaderStyle: standaloneEnums.MinimapSectionHeaderStyle, MouseTargetType: standaloneEnums.MouseTargetType, OverlayWidgetPositionPreference: standaloneEnums.OverlayWidgetPositionPreference, OverviewRulerLane: standaloneEnums.OverviewRulerLane, diff --git a/code/src/vs/editor/standalone/browser/standaloneLanguages.ts b/code/src/vs/editor/standalone/browser/standaloneLanguages.ts index 8466fbf9f99..5ade938e7c2 100644 --- a/code/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/code/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -809,6 +809,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { InlineEditTriggerKind: standaloneEnums.InlineEditTriggerKind, CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, NewSymbolNameTag: standaloneEnums.NewSymbolNameTag, + PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/code/src/vs/editor/standalone/browser/standaloneLayoutService.ts b/code/src/vs/editor/standalone/browser/standaloneLayoutService.ts index cc946b22b97..8b55de680ba 100644 --- a/code/src/vs/editor/standalone/browser/standaloneLayoutService.ts +++ b/code/src/vs/editor/standalone/browser/standaloneLayoutService.ts @@ -19,7 +19,6 @@ class StandaloneLayoutService implements ILayoutService { readonly onDidLayoutContainer = Event.None; readonly onDidChangeActiveContainer = Event.None; readonly onDidAddContainer = Event.None; - readonly whenActiveContainerStylesLoaded = Promise.resolve(); get mainContainer(): HTMLElement { return firstOrDefault(this._codeEditorService.listCodeEditors())?.getContainerDomNode() ?? mainWindow.document.body; @@ -50,6 +49,8 @@ class StandaloneLayoutService implements ILayoutService { return this.activeContainer; } + whenContainerStylesLoaded() { return undefined; } + focus(): void { this._codeEditorService.getFocusedCodeEditor()?.focus(); } diff --git a/code/src/vs/editor/standalone/browser/standaloneServices.ts b/code/src/vs/editor/standalone/browser/standaloneServices.ts index c815e1b3d13..c3d2f3c41c3 100644 --- a/code/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/code/src/vs/editor/standalone/browser/standaloneServices.ts @@ -1062,7 +1062,7 @@ class StandaloneAccessbilitySignalService implements IAccessibilitySignalService async playSignal(cue: AccessibilitySignal, options: {}): Promise { } - async playAccessibilitySignals(cues: AccessibilitySignal[]): Promise { + async playSignals(cues: AccessibilitySignal[]): Promise { } isSoundEnabled(cue: AccessibilitySignal): boolean { diff --git a/code/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/code/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index d440e01d723..5b4d0994a62 100644 --- a/code/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/code/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -4,14 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TrimTrailingWhitespaceCommand, trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { EncodedTokenizationResult, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { getEditOperation } from 'vs/editor/test/browser/testCommand'; -import { withEditorModel } from 'vs/editor/test/common/testTextModel'; +import { createModelServices, instantiateTextModel, withEditorModel } from 'vs/editor/test/common/testTextModel'; /** * Create single edit operation @@ -36,7 +41,7 @@ function createSingleEditOp(text: string | null, positionLineNumber: number, pos function assertTrimTrailingWhitespaceCommand(text: string[], expected: ISingleEditOperation[]): void { return withEditorModel(text, (model) => { - const op = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), []); + const op = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), [], true); const actual = getEditOperation(model, op); assert.deepStrictEqual(actual, expected); }); @@ -44,13 +49,23 @@ function assertTrimTrailingWhitespaceCommand(text: string[], expected: ISingleEd function assertTrimTrailingWhitespace(text: string[], cursors: Position[], expected: ISingleEditOperation[]): void { return withEditorModel(text, (model) => { - const actual = trimTrailingWhitespace(model, cursors); + const actual = trimTrailingWhitespace(model, cursors, true); assert.deepStrictEqual(actual, expected); }); } suite('Editor Commands - Trim Trailing Whitespace Command', () => { + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + ensureNoDisposablesAreLeakedInTestSuite(); test('remove trailing whitespace', function () { @@ -102,4 +117,73 @@ suite('Editor Commands - Trim Trailing Whitespace Command', () => { ]); }); + test('skips strings and regex if configured', function () { + const instantiationService = createModelServices(disposables); + const languageService = instantiationService.get(ILanguageService); + const languageId = 'testLanguageId'; + const languageIdCodec = languageService.languageIdCodec; + disposables.add(languageService.registerLanguage({ id: languageId })); + const encodedLanguageId = languageIdCodec.encodeLanguageId(languageId); + + const otherMetadata = ( + (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) + | (StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET) + | (MetadataConsts.BALANCED_BRACKETS_MASK) + ) >>> 0; + const stringMetadata = ( + (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) + | (StandardTokenType.String << MetadataConsts.TOKEN_TYPE_OFFSET) + | (MetadataConsts.BALANCED_BRACKETS_MASK) + ) >>> 0; + + const tokenizationSupport: ITokenizationSupport = { + getInitialState: () => NullState, + tokenize: undefined!, + tokenizeEncoded: (line, hasEOL, state) => { + switch (line) { + case 'const a = ` ': { + const tokens = new Uint32Array([ + 0, otherMetadata, + 10, stringMetadata, + ]); + return new EncodedTokenizationResult(tokens, state); + } + case ' a string ': { + const tokens = new Uint32Array([ + 0, stringMetadata, + ]); + return new EncodedTokenizationResult(tokens, state); + } + case '`; ': { + const tokens = new Uint32Array([ + 0, stringMetadata, + 1, otherMetadata + ]); + return new EncodedTokenizationResult(tokens, state); + } + } + throw new Error(`Unexpected`); + } + }; + + disposables.add(TokenizationRegistry.register(languageId, tokenizationSupport)); + + const model = disposables.add(instantiateTextModel( + instantiationService, + [ + 'const a = ` ', + ' a string ', + '`; ', + ].join('\n'), + languageId + )); + + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); + model.tokenization.forceTokenization(3); + + const op = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), [], false); + const actual = getEditOperation(model, op); + assert.deepStrictEqual(actual, [createSingleEditOp(null, 3, 3, 3, 5)]); + }); }); diff --git a/code/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/code/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index aef1e1cd2fa..4f644203ef4 100644 --- a/code/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/code/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts @@ -58,6 +58,9 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { maxColumn: input.minimapMaxColumn, showSlider: 'mouseover', scale: 1, + showRegionSectionHeaders: true, + showMarkSectionHeaders: true, + sectionHeaderFontSize: 9 }; options._write(EditorOption.minimap, minimapOptions); const scrollbarOptions: InternalEditorScrollbarOptions = { diff --git a/code/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/code/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts new file mode 100644 index 00000000000..39aead0a848 --- /dev/null +++ b/code/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; + +suite('PositionOffsetTransformer', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const str = '123456\nabcdef\nghijkl\nmnopqr'; + + const t = new PositionOffsetTransformer(str); + test('getPosition', () => { + assert.deepStrictEqual( + new OffsetRange(0, str.length + 2).map(i => t.getPosition(i).toString()), + [ + "(1,1)", + "(1,2)", + "(1,3)", + "(1,4)", + "(1,5)", + "(1,6)", + "(1,7)", + "(2,1)", + "(2,2)", + "(2,3)", + "(2,4)", + "(2,5)", + "(2,6)", + "(2,7)", + "(3,1)", + "(3,2)", + "(3,3)", + "(3,4)", + "(3,5)", + "(3,6)", + "(3,7)", + "(4,1)", + "(4,2)", + "(4,3)", + "(4,4)", + "(4,5)", + "(4,6)", + "(4,7)", + "(4,8)" + ] + ); + }); + + test('getOffset', () => { + for (let i = 0; i < str.length + 2; i++) { + assert.strictEqual(t.getOffset(t.getPosition(i)), i); + } + }); +}); diff --git a/code/src/vs/editor/test/common/core/random.ts b/code/src/vs/editor/test/common/core/random.ts new file mode 100644 index 00000000000..d48f4173f82 --- /dev/null +++ b/code/src/vs/editor/test/common/core/random.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 { numberComparator } from 'vs/base/common/arrays'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { Position } from 'vs/editor/common/core/position'; +import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; +import { Range } from 'vs/editor/common/core/range'; +import { AbstractText, SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; + +export abstract class Random { + public static basicAlphabet: string = ' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + public static basicAlphabetMultiline: string = ' \n\n\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + public static create(seed: number): Random { + return new MersenneTwister(seed); + } + + public abstract nextIntRange(start: number, endExclusive: number): number; + + public nextString(length: number, alphabet = Random.basicAlphabet): string { + let randomText: string = ''; + for (let i = 0; i < length; i++) { + const characterIndex = this.nextIntRange(0, alphabet.length); + randomText += alphabet.charAt(characterIndex); + } + return randomText; + } + + public nextMultiLineString(lineCount: number, lineLengthRange: OffsetRange, alphabet = Random.basicAlphabet): string { + const lines: string[] = []; + for (let i = 0; i < lineCount; i++) { + const lineLength = this.nextIntRange(lineLengthRange.start, lineLengthRange.endExclusive); + lines.push(this.nextString(lineLength, alphabet)); + } + return lines.join('\n'); + } + + public nextConsecutivePositions(source: AbstractText, count: number): Position[] { + const t = new PositionOffsetTransformer(source.getValue()); + const offsets = OffsetRange.ofLength(count).map(() => this.nextIntRange(0, t.text.length)); + offsets.sort(numberComparator); + return offsets.map(offset => t.getPosition(offset)); + } + + public nextRange(source: AbstractText): Range { + const [start, end] = this.nextConsecutivePositions(source, 2); + return Range.fromPositions(start, end); + } + + public nextTextEdit(target: AbstractText, singleTextEditCount: number): TextEdit { + const singleTextEdits: SingleTextEdit[] = []; + + const positions = this.nextConsecutivePositions(target, singleTextEditCount * 2); + + for (let i = 0; i < singleTextEditCount; i++) { + const start = positions[i * 2]; + const end = positions[i * 2 + 1]; + const newText = this.nextString(end.column - start.column, Random.basicAlphabetMultiline); + singleTextEdits.push(new SingleTextEdit(Range.fromPositions(start, end), newText)); + } + + return new TextEdit(singleTextEdits).normalize(); + } +} + +class MersenneTwister extends Random { + private readonly mt = new Array(624); + private index = 0; + + constructor(seed: number) { + super(); + + this.mt[0] = seed >>> 0; + for (let i = 1; i < 624; i++) { + const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); + this.mt[i] = (((((s & 0xffff0000) >>> 16) * 0x6c078965) << 16) + (s & 0x0000ffff) * 0x6c078965 + i) >>> 0; + } + } + + private _nextInt() { + if (this.index === 0) { + this.generateNumbers(); + } + + let y = this.mt[this.index]; + y = y ^ (y >>> 11); + y = y ^ ((y << 7) & 0x9d2c5680); + y = y ^ ((y << 15) & 0xefc60000); + y = y ^ (y >>> 18); + + this.index = (this.index + 1) % 624; + + return y >>> 0; + } + + public nextIntRange(start: number, endExclusive: number) { + const range = endExclusive - start; + return Math.floor(this._nextInt() / (0x100000000 / range)) + start; + } + + private generateNumbers() { + for (let i = 0; i < 624; i++) { + const y = (this.mt[i] & 0x80000000) + (this.mt[(i + 1) % 624] & 0x7fffffff); + this.mt[i] = this.mt[(i + 397) % 624] ^ (y >>> 1); + if ((y % 2) !== 0) { + this.mt[i] = this.mt[i] ^ 0x9908b0df; + } + } + } +} diff --git a/code/src/vs/editor/test/common/core/textEdit.test.ts b/code/src/vs/editor/test/common/core/textEdit.test.ts new file mode 100644 index 00000000000..f02e8a9bd50 --- /dev/null +++ b/code/src/vs/editor/test/common/core/textEdit.test.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 assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { StringText } from 'vs/editor/common/core/textEdit'; +import { Random } from 'vs/editor/test/common/core/random'; + +suite('TextEdit', () => { + suite('inverse', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function runTest(seed: number): void { + const rand = Random.create(seed); + const source = new StringText(rand.nextMultiLineString(10, new OffsetRange(0, 10))); + + const edit = rand.nextTextEdit(source, rand.nextIntRange(1, 5)); + const invEdit = edit.inverse(source); + + const s1 = edit.apply(source); + const s2 = invEdit.applyToString(s1); + + assert.deepStrictEqual(s2, source.value); + } + + test.skip('brute-force', () => { + for (let i = 0; i < 100_000; i++) { + runTest(i); + } + }); + + for (let seed = 0; seed < 20; seed++) { + test(`test ${seed}`, () => runTest(seed)); + } + }); +}); diff --git a/code/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/code/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index 611ba267d5b..9155b32a9ca 100644 --- a/code/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/code/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -5,16 +5,16 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper'; import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos'; import { lengthAdd, lengthToObj, lengthToPosition, positionToLength, toLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { Random } from 'vs/editor/test/common/core/random'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; suite('combineTextEditInfos', () => { - ensureNoDisposablesAreLeakedInTestSuite(); for (let seed = 0; seed < 50; seed++) { @@ -25,7 +25,7 @@ suite('combineTextEditInfos', () => { }); function runTest(seed: number) { - const rng = new MersenneTwister(seed); + const rng = Random.create(seed); const str = 'abcde\nfghij\nklmno\npqrst\n'; const textModelS0 = createTextModel(str); @@ -58,7 +58,7 @@ function runTest(seed: number) { textModelS2.dispose(); } -export function getRandomEditInfos(textModel: TextModel, count: number, rng: MersenneTwister, disjoint: boolean = false): TextEditInfo[] { +export function getRandomEditInfos(textModel: TextModel, count: number, rng: Random, disjoint: boolean = false): TextEditInfo[] { const edits: TextEditInfo[] = []; let i = 0; for (let j = 0; j < count; j++) { @@ -68,7 +68,7 @@ export function getRandomEditInfos(textModel: TextModel, count: number, rng: Mer return edits; } -function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: MersenneTwister): TextEditInfo { +function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: Random): TextEditInfo { const textModelLength = textModel.getValueLength(); const offsetStart = rng.nextIntRange(rangeOffsetStart, textModelLength); const offsetEnd = rng.nextIntRange(offsetStart, textModelLength); @@ -79,7 +79,7 @@ function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: Mers return new TextEditInfo(positionToLength(textModel.getPositionAt(offsetStart)), positionToLength(textModel.getPositionAt(offsetEnd)), toLength(lineCount, columnCount)); } -export function toEdit(editInfo: TextEditInfo): ISingleEditOperation { +function toEdit(editInfo: TextEditInfo): SingleTextEdit { const l = lengthToObj(editInfo.newLength); let text = ''; @@ -90,56 +90,11 @@ export function toEdit(editInfo: TextEditInfo): ISingleEditOperation { text += 'C'; } - return { - range: Range.fromPositions( + return new SingleTextEdit( + Range.fromPositions( lengthToPosition(editInfo.startOffset), lengthToPosition(editInfo.endOffset) ), text - }; -} - -// Generated by copilot -export class MersenneTwister { - private readonly mt = new Array(624); - private index = 0; - - constructor(seed: number) { - this.mt[0] = seed >>> 0; - for (let i = 1; i < 624; i++) { - const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); - this.mt[i] = (((((s & 0xffff0000) >>> 16) * 0x6c078965) << 16) + (s & 0x0000ffff) * 0x6c078965 + i) >>> 0; - } - } - - public nextInt() { - if (this.index === 0) { - this.generateNumbers(); - } - - let y = this.mt[this.index]; - y = y ^ (y >>> 11); - y = y ^ ((y << 7) & 0x9d2c5680); - y = y ^ ((y << 15) & 0xefc60000); - y = y ^ (y >>> 18); - - this.index = (this.index + 1) % 624; - - return y >>> 0; - } - - public nextIntRange(start: number, endExclusive: number) { - const range = endExclusive - start; - return Math.floor(this.nextInt() / (0x100000000 / range)) + start; - } - - private generateNumbers() { - for (let i = 0; i < 624; i++) { - const y = (this.mt[i] & 0x80000000) + (this.mt[(i + 1) % 624] & 0x7fffffff); - this.mt[i] = this.mt[(i + 397) % 624] ^ (y >>> 1); - if ((y % 2) !== 0) { - this.mt[i] = this.mt[i] ^ 0x9908b0df; - } - } - } + ); } diff --git a/code/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/code/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 262032dd9e4..007033dc790 100644 --- a/code/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/code/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -2056,7 +2056,7 @@ suite('chunk based search', () => { ds.add(pieceTree); const pieceTable = pieceTree.getPieceTree(); pieceTable.delete(0, 1); - const ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 1, 1), new SearchData(/abc/, new WordCharacterClassifier(',./'), 'abc'), true, 1000); + const ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 1, 1), new SearchData(/abc/, new WordCharacterClassifier(',./', []), 'abc'), true, 1000); assert.strictEqual(ret.length, 0); }); @@ -2078,7 +2078,7 @@ suite('chunk based search', () => { pieceTable.delete(16, 1); pieceTable.insert(16, ' '); - const ret = pieceTable.findMatchesLineByLine(new Range(1, 1, 4, 13), new SearchData(/\[/gi, new WordCharacterClassifier(',./'), '['), true, 1000); + const ret = pieceTable.findMatchesLineByLine(new Range(1, 1, 4, 13), new SearchData(/\[/gi, new WordCharacterClassifier(',./', []), '['), true, 1000); assert.strictEqual(ret.length, 3); assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); diff --git a/code/src/vs/editor/test/common/model/textModelSearch.test.ts b/code/src/vs/editor/test/common/model/textModelSearch.test.ts index 16d237c00a5..91ec41810f3 100644 --- a/code/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/code/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -19,7 +19,7 @@ suite('TextModelSearch', () => { ensureNoDisposablesAreLeakedInTestSuite(); - const usualWordSeparators = getMapForWordSeparators(USUAL_WORD_SEPARATORS); + const usualWordSeparators = getMapForWordSeparators(USUAL_WORD_SEPARATORS, []); function assertFindMatch(actual: FindMatch | null, expectedRange: Range, expectedMatches: string[] | null = null): void { assert.deepStrictEqual(actual, new FindMatch(expectedRange, expectedMatches)); diff --git a/code/src/vs/editor/test/common/services/testEditorWorkerService.ts b/code/src/vs/editor/test/common/services/testEditorWorkerService.ts index e6693d821e9..e7d5154f9f6 100644 --- a/code/src/vs/editor/test/common/services/testEditorWorkerService.ts +++ b/code/src/vs/editor/test/common/services/testEditorWorkerService.ts @@ -9,6 +9,7 @@ import { DiffAlgorithmName, IEditorWorkerService, IUnicodeHighlightsResult } fro import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/languages'; import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider'; import { IChange } from 'vs/editor/common/diff/legacyLinesDiffComputer'; +import { SectionHeader } from 'vs/editor/common/services/findSectionHeaders'; export class TestEditorWorkerService implements IEditorWorkerService { @@ -25,4 +26,5 @@ export class TestEditorWorkerService implements IEditorWorkerService { async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> { return null; } canNavigateValueSet(resource: URI): boolean { return false; } async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise { return null; } + async findSectionHeaders(uri: URI): Promise { return []; } } diff --git a/code/src/vs/monaco.d.ts b/code/src/vs/monaco.d.ts index c094df337af..509f549d41b 100644 --- a/code/src/vs/monaco.d.ts +++ b/code/src/vs/monaco.d.ts @@ -1613,6 +1613,14 @@ declare namespace monaco.editor { Gutter = 2 } + /** + * Section header style. + */ + export enum MinimapSectionHeaderStyle { + Normal = 1, + Underlined = 2 + } + export interface IDecorationOptions { /** * CSS color to render. @@ -1656,6 +1664,14 @@ declare namespace monaco.editor { * The position in the minimap. */ position: MinimapPosition; + /** + * If the decoration is for a section header, which header style. + */ + sectionHeaderStyle?: MinimapSectionHeaderStyle | null; + /** + * If the decoration is for a section header, the header text. + */ + sectionHeaderText?: string | null; } /** @@ -3102,6 +3118,13 @@ declare namespace monaco.editor { * Defaults to empty array. */ rulers?: (number | IRulerOption)[]; + /** + * Locales used for segmenting lines into words when doing word related navigations or operations. + * + * Specify the BCP 47 language tag of the word you wish to recognize (e.g., ja, zh-CN, zh-Hant-TW, etc.). + * Defaults to empty array + */ + wordSegmenterLocales?: string | string[]; /** * A string containing the word separators used when doing word navigation. * Defaults to `~!@#$%^&*()-=+[{]}\\|;:\'",.<>/? @@ -3818,6 +3841,10 @@ declare namespace monaco.editor { * Default to true. */ renderMarginRevertIcon?: boolean; + /** + * Indicates if the gutter menu should be rendered. + */ + renderGutterMenu?: boolean; /** * Original model should be editable? * Defaults to false. @@ -4279,6 +4306,18 @@ declare namespace monaco.editor { * Relative size of the font in the minimap. Defaults to 1. */ scale?: number; + /** + * Whether to show named regions as section headers. Defaults to true. + */ + showRegionSectionHeaders?: boolean; + /** + * Whether to show MARK: comments as section headers. Defaults to true. + */ + showMarkSectionHeaders?: boolean; + /** + * Font size of section headers. Defaults to 9. + */ + sectionHeaderFontSize?: number; } /** @@ -4928,25 +4967,26 @@ declare namespace monaco.editor { useShadowDOM = 127, useTabStops = 128, wordBreak = 129, - wordSeparators = 130, - wordWrap = 131, - wordWrapBreakAfterCharacters = 132, - wordWrapBreakBeforeCharacters = 133, - wordWrapColumn = 134, - wordWrapOverride1 = 135, - wordWrapOverride2 = 136, - wrappingIndent = 137, - wrappingStrategy = 138, - showDeprecated = 139, - inlayHints = 140, - editorClassName = 141, - pixelRatio = 142, - tabFocusMode = 143, - layoutInfo = 144, - wrappingInfo = 145, - defaultColorDecorators = 146, - colorDecoratorsActivatedOn = 147, - inlineCompletionsAccessibilityVerbose = 148 + wordSegmenterLocales = 130, + wordSeparators = 131, + wordWrap = 132, + wordWrapBreakAfterCharacters = 133, + wordWrapBreakBeforeCharacters = 134, + wordWrapColumn = 135, + wordWrapOverride1 = 136, + wordWrapOverride2 = 137, + wrappingIndent = 138, + wrappingStrategy = 139, + showDeprecated = 140, + inlayHints = 141, + editorClassName = 142, + pixelRatio = 143, + tabFocusMode = 144, + layoutInfo = 145, + wrappingInfo = 146, + defaultColorDecorators = 147, + colorDecoratorsActivatedOn = 148, + inlineCompletionsAccessibilityVerbose = 149 } export const EditorOptions: { @@ -5084,6 +5124,7 @@ declare namespace monaco.editor { useShadowDOM: IEditorOption; useTabStops: IEditorOption; wordBreak: IEditorOption; + wordSegmenterLocales: IEditorOption; wordSeparators: IEditorOption; wordWrap: IEditorOption; wordWrapBreakAfterCharacters: IEditorOption; @@ -5611,6 +5652,7 @@ declare namespace monaco.editor { export interface IPasteEvent { readonly range: Range; readonly languageId: string | null; + readonly clipboardEvent?: ClipboardEvent; } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions, IEditorConstructionOptions { @@ -6956,6 +6998,22 @@ declare namespace monaco.languages { dispose?(): void; } + /** + * Info provided on partial acceptance. + */ + export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; + } + + /** + * How a partial acceptance was triggered. + */ + export enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2 + } + /** * How a suggest provider was triggered. */ @@ -7102,7 +7160,7 @@ declare namespace monaco.languages { /** * Will be called when an item is partially accepted. */ - handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number): void; + handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number, info: PartialAcceptInfo): void; /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ @@ -7822,7 +7880,7 @@ declare namespace monaco.languages { body: string; range: IRange | undefined; uri: Uri; - owner: string; + uniqueOwner: string; isReply: boolean; } diff --git a/code/src/vs/nls.mock.ts b/code/src/vs/nls.mock.ts index d9ee1ecd2c6..5323c6c6340 100644 --- a/code/src/vs/nls.mock.ts +++ b/code/src/vs/nls.mock.ts @@ -8,7 +8,7 @@ export interface ILocalizeInfo { comment: string[]; } -interface ILocalizedString { +export interface ILocalizedString { original: string; value: string; } diff --git a/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index d2689b002d7..de4044b74f4 100644 --- a/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -17,20 +17,25 @@ export const IAccessibilitySignalService = createDecorator; - playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise; - isSoundEnabled(cue: AccessibilitySignal): boolean; - isAnnouncementEnabled(cue: AccessibilitySignal): boolean; - onSoundEnabledChanged(cue: AccessibilitySignal): Event; - onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event; - - playSound(cue: Sound, allowManyInParallel?: boolean): Promise; - playSignalLoop(cue: AccessibilitySignal, milliseconds: number): IDisposable; + playSignal(signal: AccessibilitySignal, options?: IAccessbilitySignalOptions): Promise; + playSignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise; + isSoundEnabled(signal: AccessibilitySignal): boolean; + isAnnouncementEnabled(signal: AccessibilitySignal): boolean; + onSoundEnabledChanged(signal: AccessibilitySignal): Event; + onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event; + + playSound(signal: Sound, allowManyInParallel?: boolean): Promise; + playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; } export interface IAccessbilitySignalOptions { allowManyInParallel?: boolean; + + /** + * The source that triggered the signal (e.g. "diffEditor.cursorPositionChanged"). + */ source?: string; + /** * For actions like save or format, depending on the * configured value, we will only @@ -57,9 +62,9 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } public async playSignal(signal: AccessibilitySignal, options: IAccessbilitySignalOptions = {}): Promise { - const alertMessage = signal.announcementMessage; - if (this.isAnnouncementEnabled(signal, options.userGesture) && alertMessage) { - this.accessibilityService.status(alertMessage); + const announcementMessage = signal.announcementMessage; + if (this.isAnnouncementEnabled(signal, options.userGesture) && announcementMessage) { + this.accessibilityService.status(announcementMessage); } if (this.isSoundEnabled(signal, options.userGesture)) { @@ -68,26 +73,26 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } } - public async playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise { - for (const cue of cues) { - this.sendSignalTelemetry('cue' in cue ? cue.cue : cue, 'source' in cue ? cue.source : undefined); + public async playSignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise { + for (const signal of signals) { + this.sendSignalTelemetry('signal' in signal ? signal.signal : signal, 'source' in signal ? signal.source : undefined); } - const cueArray = cues.map(c => 'cue' in c ? c.cue : c); - const alerts = cueArray.filter(cue => this.isAnnouncementEnabled(cue)).map(c => c.announcementMessage); - if (alerts.length) { - this.accessibilityService.status(alerts.join(', ')); + const signalArray = signals.map(s => 'signal' in s ? s.signal : s); + const announcements = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); + if (announcements.length) { + this.accessibilityService.status(announcements.join(', ')); } // Some sounds are reused. Don't play the same sound twice. - const sounds = new Set(cueArray.filter(cue => this.isSoundEnabled(cue)).map(cue => cue.sound.getSound())); + const sounds = new Set(signalArray.filter(signal => this.isSoundEnabled(signal)).map(signal => signal.sound.getSound())); await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true))); } - private sendSignalTelemetry(cue: AccessibilitySignal, source: string | undefined): void { + private sendSignalTelemetry(signal: AccessibilitySignal, source: string | undefined): void { const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized(); - const key = cue.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); + const key = signal.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); // Only send once per user session if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) { return; @@ -107,7 +112,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi comment: 'This data is collected to understand how signals are used and if more signals should be added.'; }>('signal.played', { - signal: cue.name, + signal: signal.name, source: source ?? '', isScreenReaderOptimized, }); @@ -214,7 +219,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi () => event.signal.announcementMessage ? this.configurationService.getValue<'auto' | 'off' | 'userGesture' | 'always' | 'never'>(event.signal.settingsKey + '.announcement') : false ); return derived(reader => { - /** @description alert enabled */ + /** @description announcement enabled */ const setting = settingObservable.read(reader); if ( !this.screenReaderAttached.read(reader) @@ -240,8 +245,8 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return Event.fromObservableLight(this.isSoundEnabledCache.get({ signal })); } - public onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event { - return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal: cue })); + public onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event { + return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal })); } } @@ -314,6 +319,8 @@ export class Sound { public static readonly clear = Sound.register({ fileName: 'clear.mp3' }); public static readonly save = Sound.register({ fileName: 'save.mp3' }); public static readonly format = Sound.register({ fileName: 'format.mp3' }); + public static readonly voiceRecordingStarted = Sound.register({ fileName: 'voiceRecordingStarted.mp3' }); + public static readonly voiceRecordingStopped = Sound.register({ fileName: 'voiceRecordingStopped.mp3' }); private constructor(public readonly fileName: string) { } } @@ -582,6 +589,20 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.format' }); + public static readonly voiceRecordingStarted = AccessibilitySignal.register({ + name: localize('accessibilitySignals.voiceRecordingStarted', 'Voice Recording Started'), + sound: Sound.voiceRecordingStarted, + legacySoundSettingsKey: 'audioCues.voiceRecordingStarted', + settingsKey: 'accessibility.signals.voiceRecordingStarted' + }); + + public static readonly voiceRecordingStopped = AccessibilitySignal.register({ + name: localize('accessibilitySignals.voiceRecordingStopped', 'Voice Recording Stopped'), + sound: Sound.voiceRecordingStopped, + legacySoundSettingsKey: 'audioCues.voiceRecordingStopped', + settingsKey: 'accessibility.signals.voiceRecordingStopped' + }); + private constructor( public readonly sound: SoundSource, public readonly name: string, diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 new file mode 100644 index 00000000000..488754fdd58 Binary files /dev/null and b/code/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 differ diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 new file mode 100644 index 00000000000..0532cf6b15a Binary files /dev/null and b/code/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 differ diff --git a/code/src/vs/platform/actionWidget/browser/actionList.ts b/code/src/vs/platform/actionWidget/browser/actionList.ts index 4b0b93f695d..80f8e74664e 100644 --- a/code/src/vs/platform/actionWidget/browser/actionList.ts +++ b/code/src/vs/platform/actionWidget/browser/actionList.ts @@ -140,7 +140,7 @@ class ActionItemRenderer implements IListRenderer, IAction } disposeTemplate(_templateData: IActionMenuTemplateData): void { - // noop + _templateData.keybinding.dispose(); } } diff --git a/code/src/vs/platform/actions/browser/buttonbar.ts b/code/src/vs/platform/actions/browser/buttonbar.ts index 94720d5b557..79cbe6faa37 100644 --- a/code/src/vs/platform/actions/browser/buttonbar.ts +++ b/code/src/vs/platform/actions/browser/buttonbar.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ActionRunner, IAction, IActionRunner, SubmenuAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -73,7 +73,7 @@ export class WorkbenchButtonBar extends ButtonBar { this.clear(); // Support instamt hover between buttons - const hoverDelegate = this._updateStore.add(getDefaultHoverDelegate('element', true)); + const hoverDelegate = this._updateStore.add(createInstantHoverDelegate()); for (let i = 0; i < actions.length; i++) { diff --git a/code/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts b/code/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index cfcc0e2c73b..97aac13854a 100644 --- a/code/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts +++ b/code/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -19,7 +19,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; export interface IDropdownWithPrimaryActionViewItemOptions { actionRunner?: IActionRunner; diff --git a/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index c1edd287bea..da596a8fc6c 100644 --- a/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/code/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -26,7 +26,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; import { isDark } from 'vs/platform/theme/common/theme'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { assertType } from 'vs/base/common/types'; import { asCssVariable, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; diff --git a/code/src/vs/platform/actions/common/actions.ts b/code/src/vs/platform/actions/common/actions.ts index 98cd2259f98..6728ef70a71 100644 --- a/code/src/vs/platform/actions/common/actions.ts +++ b/code/src/vs/platform/actions/common/actions.ts @@ -162,6 +162,7 @@ export class MenuId { static readonly CommentThreadCommentContext = new MenuId('CommentThreadCommentContext'); static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); + static readonly CommentsViewThreadActions = new MenuId('CommentsViewThreadActions'); static readonly InteractiveToolbar = new MenuId('InteractiveToolbar'); static readonly InteractiveCellTitle = new MenuId('InteractiveCellTitle'); static readonly InteractiveCellDelete = new MenuId('InteractiveCellDelete'); @@ -182,6 +183,8 @@ export class MenuId { static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar'); + static readonly NotebookOutlineFilter = new MenuId('NotebookOutlineFilter'); + static readonly NotebookOutlineActionMenu = new MenuId('NotebookOutlineActionMenu'); static readonly NotebookEditorLayoutConfigure = new MenuId('NotebookEditorLayoutConfigure'); static readonly NotebookKernelSource = new MenuId('NotebookKernelSource'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); @@ -194,6 +197,7 @@ export class MenuId { static readonly SidebarTitle = new MenuId('SidebarTitle'); static readonly PanelTitle = new MenuId('PanelTitle'); static readonly AuxiliaryBarTitle = new MenuId('AuxiliaryBarTitle'); + static readonly AuxiliaryBarHeader = new MenuId('AuxiliaryBarHeader'); static readonly TerminalInstanceContext = new MenuId('TerminalInstanceContext'); static readonly TerminalEditorInstanceContext = new MenuId('TerminalEditorInstanceContext'); static readonly TerminalNewDropdownContext = new MenuId('TerminalNewDropdownContext'); @@ -217,6 +221,9 @@ export class MenuId { static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly AccessibleView = new MenuId('AccessibleView'); static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar'); + static readonly DiffEditorHunkToolbar = new MenuId('DiffEditorHunkToolbar'); + static readonly DiffEditorSelectionToolbar = new MenuId('DiffEditorSelectionToolbar'); + /** * Create or reuse a `MenuId` with the given identifier diff --git a/code/src/vs/platform/contextview/browser/contextView.ts b/code/src/vs/platform/contextview/browser/contextView.ts index 10158c8d75c..87c58811d8d 100644 --- a/code/src/vs/platform/contextview/browser/contextView.ts +++ b/code/src/vs/platform/contextview/browser/contextView.ts @@ -43,6 +43,9 @@ export interface IContextViewDelegate { focus?(): void; anchorAlignment?: AnchorAlignment; anchorAxisAlignment?: AnchorAxisAlignment; + + // context views with higher layers are rendered over contet views with lower layers + layer?: number; // Default: 0 } export const IContextMenuService = createDecorator('contextMenuService'); diff --git a/code/src/vs/platform/contextview/browser/contextViewService.ts b/code/src/vs/platform/contextview/browser/contextViewService.ts index f47285746fe..beaf0b894e6 100644 --- a/code/src/vs/platform/contextview/browser/contextViewService.ts +++ b/code/src/vs/platform/contextview/browser/contextViewService.ts @@ -3,18 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContextView, ContextViewDOMPosition } from 'vs/base/browser/ui/contextview/contextview'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ContextView, ContextViewDOMPosition, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IContextViewDelegate, IContextViewService } from './contextView'; import { getWindow } from 'vs/base/browser/dom'; -export class ContextViewService extends Disposable implements IContextViewService { - declare readonly _serviceBrand: undefined; +export class ContextViewHandler extends Disposable implements IContextViewProvider { - private currentViewDisposable: IDisposable = Disposable.None; - private readonly contextView = this._register(new ContextView(this.layoutService.mainContainer, ContextViewDOMPosition.ABSOLUTE)); + private currentViewDisposable = this._register(new MutableDisposable()); + protected readonly contextView = this._register(new ContextView(this.layoutService.mainContainer, ContextViewDOMPosition.ABSOLUTE)); constructor( @ILayoutService private readonly layoutService: ILayoutService @@ -51,14 +50,10 @@ export class ContextViewService extends Disposable implements IContextViewServic } }); - this.currentViewDisposable = disposable; + this.currentViewDisposable.value = disposable; return disposable; } - getContextViewElement(): HTMLElement { - return this.contextView.getViewElement(); - } - layout(): void { this.contextView.layout(); } @@ -66,11 +61,13 @@ export class ContextViewService extends Disposable implements IContextViewServic hideContextView(data?: any): void { this.contextView.hide(data); } +} + +export class ContextViewService extends ContextViewHandler implements IContextViewService { - override dispose(): void { - super.dispose(); + declare readonly _serviceBrand: undefined; - this.currentViewDisposable.dispose(); - this.currentViewDisposable = Disposable.None; + getContextViewElement(): HTMLElement { + return this.contextView.getViewElement(); } } diff --git a/code/src/vs/platform/editor/common/editor.ts b/code/src/vs/platform/editor/common/editor.ts index 24e2e4c5506..159bea6fc8e 100644 --- a/code/src/vs/platform/editor/common/editor.ts +++ b/code/src/vs/platform/editor/common/editor.ts @@ -288,6 +288,19 @@ export interface IEditorOptions { * applied when opening the editor. */ viewState?: object; + + /** + * A transient editor will attempt to appear as preview and certain components + * (such as history tracking) may decide to ignore the editor when it becomes + * active. + * This option is meant to be used only when the editor is used for a short + * period of time, for example when opening a preview of the editor from a + * picker control in the background while navigating through results of the picker. + * + * Note: an editor that is already opened in a group that is not transient, will + * not turn transient. + */ + transient?: boolean; } export interface ITextEditorSelection { diff --git a/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index ced410cb52e..f7c4a89d735 100644 --- a/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -16,7 +16,8 @@ import * as nls from 'vs/nls'; import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, - InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError + InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -27,9 +28,9 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export type ExtensionVerificationStatus = boolean | string; -export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions & InstallVSIXOptions }; +export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions }; -export type InstallExtensionTaskOptions = InstallOptions & InstallVSIXOptions & { readonly profileLocation: URI }; +export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI; readonly productVersion: IProductVersion }; export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; @@ -124,7 +125,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl await Promise.allSettled(extensions.map(async ({ extension, options }) => { try { - const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion); + const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date }); installableExtensions.push({ ...compatible, options }); } catch (error) { results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); @@ -228,9 +229,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const isApplicationScoped = options.isApplicationScoped || options.isBuiltin || isApplicationScopedExtension(manifest); const installExtensionTaskOptions: InstallExtensionTaskOptions = { ...options, - installOnlyNewlyAddedFromExtensionPack: URI.isUri(extension) ? options.installOnlyNewlyAddedFromExtensionPack : true, /* always true for gallery extensions */ + installOnlyNewlyAddedFromExtensionPack: options.installOnlyNewlyAddedFromExtensionPack ?? !URI.isUri(extension) /* always true for gallery extensions */, isApplicationScoped, - profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation() + profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(), + productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }; const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined; @@ -248,8 +250,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation); - const installed = await this.getInstalled(undefined, task.options.profileLocation); + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion); + const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion); const options: InstallExtensionTaskOptions = { ...task.options, donotIncludePackAndDependencies: true, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { if (installingExtensionsMap.has(`${gallery.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`)) { @@ -405,12 +407,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return results; } - private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { + private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { if (!this.galleryService.isEnabled()) { return []; } - const installed = await this.getInstalled(undefined, profile); + const installed = await this.getInstalled(undefined, profile, productVersion); const knownIdentifiers: IExtensionIdentifier[] = []; const allDependenciesAndPacks: { gallery: IGalleryExtension; manifest: IExtensionManifest }[] = []; @@ -442,7 +444,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier)); let compatible; try { - compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease); + compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease, productVersion); } catch (error) { if (!isDependency) { this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, getErrorMessage(error)); @@ -462,7 +464,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return allDependenciesAndPacks; } - private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { + private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean, productVersion: IProductVersion): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { let compatibleExtension: IGalleryExtension | null; const extensionsControlManifest = await this.getExtensionsControlManifest(); @@ -473,7 +475,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const deprecationInfo = extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()]; if (deprecationInfo?.extension?.autoMigrate) { this.logService.info(`The '${extension.identifier.id}' extension is deprecated, fetching the compatible '${deprecationInfo.extension.id}' extension instead.`); - compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true }, CancellationToken.None))[0]; + compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true, productVersion }, CancellationToken.None))[0]; if (!compatibleExtension) { throw new ExtensionManagementError(nls.localize('notFoundDeprecatedReplacementExtension', "Can't install '{0}' extension since it was deprecated and the replacement extension '{1}' can't be found.", extension.identifier.id, deprecationInfo.extension.id), ExtensionManagementErrorCode.Deprecated); } @@ -485,7 +487,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform); } - compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease); + compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion); if (!compatibleExtension) { /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { @@ -508,23 +510,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return { extension: compatibleExtension, manifest }; } - protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { + protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise { const targetPlatform = await this.getTargetPlatform(); let compatibleExtension: IGalleryExtension | null = null; if (!sameVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } - if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) { + if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform, productVersion)) { compatibleExtension = extension; } if (!compatibleExtension) { if (sameVersion) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } else { - compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform); + compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform, productVersion); } } @@ -715,10 +717,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract zip(extension: ILocalExtension): Promise; abstract unzip(zipLocation: URI): Promise; abstract getManifest(vsix: URI): Promise; - abstract install(vsix: URI, options?: InstallVSIXOptions): Promise; + abstract install(vsix: URI, options?: InstallOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; - abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + abstract getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; abstract download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; abstract reinstallFromGallery(extension: ILocalExtension): Promise; diff --git a/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 8833103e59c..0c9ec9e98e8 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -295,6 +295,7 @@ type GalleryServiceAdditionalQueryEvent = { }; interface IExtensionCriteria { + readonly productVersion: IProductVersion; readonly targetPlatform: TargetPlatform; readonly compatible: boolean; readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[]; @@ -662,14 +663,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi query = query.withSource(options.source); } - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); if (options.source) { extensions.forEach((e, index) => setTelemetry(e, index, options.source)); } return extensions; } - async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) { return null; } @@ -680,11 +681,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi .withFlags(Flags.IncludeVersions) .withPage(1, 1) .withFilter(FilterType.ExtensionId, extension.identifier.uuid); - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease }, CancellationToken.None); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease, productVersion }, CancellationToken.None); return extensions[0] || null; } - async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { return false; } @@ -702,10 +703,10 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } engine = manifest.engines.vscode; } - return isEngineValid(engine, this.productService.version, this.productService.date); + return isEngineValid(engine, productVersion.version, productVersion.date); } - private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise { + private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) { return false; } @@ -716,8 +717,8 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (compatible) { try { - const engine = await this.getEngine(rawGalleryExtensionVersion); - if (!isEngineValid(engine, this.productService.version, this.productService.date)) { + const engine = await this.getEngine(extension, rawGalleryExtensionVersion); + if (!isEngineValid(engine, productVersion.version, productVersion.date)) { return false; } } catch (error) { @@ -784,7 +785,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } const runQuery = async (query: Query, token: CancellationToken) => { - const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease }, token); + const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source)); return { extensions, total }; }; @@ -913,7 +914,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform)) { + if (await this.isValidVersion(getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { @@ -1045,7 +1046,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } : extension.assets.download; const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; - const context = await this.getAsset(downloadAsset, headers ? { headers } : undefined); + const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); await this.fileService.writeFile(location, context.stream); log(new Date().getTime() - startTime); } @@ -1057,13 +1058,13 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); - const context = await this.getAsset(extension.assets.signature); + const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature); await this.fileService.writeFile(location, context.stream); } async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.readme) { - const context = await this.getAsset(extension.assets.readme, {}, token); + const context = await this.getAsset(extension.identifier.id, extension.assets.readme, AssetType.Details, {}, token); const content = await asTextOrError(context); return content || ''; } @@ -1072,27 +1073,27 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.manifest) { - const context = await this.getAsset(extension.assets.manifest, {}, token); + const context = await this.getAsset(extension.identifier.id, extension.assets.manifest, AssetType.Manifest, {}, token); const text = await asTextOrError(context); return text ? JSON.parse(text) : null; } return null; } - private async getManifestFromRawExtensionVersion(rawExtensionVersion: IRawGalleryExtensionVersion, token: CancellationToken): Promise { + private async getManifestFromRawExtensionVersion(extension: string, rawExtensionVersion: IRawGalleryExtensionVersion, token: CancellationToken): Promise { const manifestAsset = getVersionAsset(rawExtensionVersion, AssetType.Manifest); if (!manifestAsset) { throw new Error('Manifest was not found'); } const headers = { 'Accept-Encoding': 'gzip' }; - const context = await this.getAsset(manifestAsset, { headers }); + const context = await this.getAsset(extension, manifestAsset, AssetType.Manifest, { headers }); return await asJson(context); } async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise { const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0]; if (asset) { - const context = await this.getAsset(asset[1]); + const context = await this.getAsset(extension.identifier.id, asset[1], asset[0]); const text = await asTextOrError(context); return text ? JSON.parse(text) : null; } @@ -1101,7 +1102,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.changelog) { - const context = await this.getAsset(extension.assets.changelog, {}, token); + const context = await this.getAsset(extension.identifier.id, extension.assets.changelog, AssetType.Changelog, {}, token); const content = await asTextOrError(context); return content || ''; } @@ -1132,7 +1133,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const validVersions: IRawGalleryExtensionVersion[] = []; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { - if (await this.isValidVersion(version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) { + if (await this.isValidVersion(extension.identifier.id, version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) { validVersions.push(version); } } catch (error) { /* Ignore error and skip version */ } @@ -1150,7 +1151,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return result; } - private async getAsset(asset: IGalleryExtensionAsset, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise { + private async getAsset(extension: string, asset: IGalleryExtensionAsset, assetType: string, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise { const commonHeaders = await this.commonHeadersPromise; const baseOptions = { type: 'GET' }; const headers = { ...commonHeaders, ...(options.headers || {}) }; @@ -1176,24 +1177,37 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi type GalleryServiceCDNFallbackClassification = { owner: 'sandy081'; comment: 'Fallback request information when the primary asset request to CDN fails'; - url: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'asset url that failed' }; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' }; + assetType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'asset that failed' }; message: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error message' }; }; type GalleryServiceCDNFallbackEvent = { - url: string; + extension: string; + assetType: string; message: string; }; - this.telemetryService.publicLog2('galleryService:cdnFallback', { url, message }); + this.telemetryService.publicLog2('galleryService:cdnFallback', { extension, assetType, message }); const fallbackOptions = { ...options, url: fallbackUrl }; return this.requestService.request(fallbackOptions, token); } } - private async getEngine(rawExtensionVersion: IRawGalleryExtensionVersion): Promise { + private async getEngine(extension: string, rawExtensionVersion: IRawGalleryExtensionVersion): Promise { let engine = getEngine(rawExtensionVersion); if (!engine) { - const manifest = await this.getManifestFromRawExtensionVersion(rawExtensionVersion, CancellationToken.None); + type GalleryServiceEngineFallbackClassification = { + owner: 'sandy081'; + comment: 'Fallback request when engine is not found in properties of an extension version'; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' }; + version: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' }; + }; + type GalleryServiceEngineFallbackEvent = { + extension: string; + version: string; + }; + this.telemetryService.publicLog2('galleryService:engineFallback', { extension, version: rawExtensionVersion.version }); + const manifest = await this.getManifestFromRawExtensionVersion(extension, rawExtensionVersion, CancellationToken.None); if (!manifest) { throw new Error('Manifest was not found'); } diff --git a/code/src/vs/platform/extensionManagement/common/extensionManagement.ts b/code/src/vs/platform/extensionManagement/common/extensionManagement.ts index 756acf86b45..9dae82eba07 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -20,6 +20,11 @@ export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export interface IProductVersion { + readonly version: string; + readonly date?: string; +} + export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { case TargetPlatform.WIN32_X64: return 'Windows 64 bit'; @@ -222,6 +227,8 @@ export interface IGalleryExtension { supportLink?: string; } +export type InstallSource = 'gallery' | 'vsix' | 'resource'; + export interface IGalleryMetadata { id: string; publisherId: string; @@ -240,9 +247,11 @@ export type Metadata = Partial; export interface ILocalExtension extends IExtension { + isWorkspaceScoped: boolean; isMachineScoped: boolean; isApplicationScoped: boolean; publisherId: string | null; @@ -253,6 +262,7 @@ export interface ILocalExtension extends IExtension { preRelease: boolean; updated: boolean; pinned: boolean; + source: InstallSource; } export const enum SortBy { @@ -281,6 +291,7 @@ export interface IQueryOptions { sortOrder?: SortOrder; source?: string; includePreRelease?: boolean; + productVersion?: IProductVersion; } export const enum StatisticType { @@ -330,6 +341,7 @@ export interface IExtensionInfo extends IExtensionIdentifier { export interface IExtensionQueryOptions { targetPlatform?: TargetPlatform; + productVersion?: IProductVersion; compatible?: boolean; queryAllVersions?: boolean; source?: string; @@ -347,8 +359,8 @@ export interface IExtensionGalleryService { query(options: IQueryOptions, token: CancellationToken): Promise>; getExtensions(extensionInfos: ReadonlyArray, token: CancellationToken): Promise; getExtensions(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, token: CancellationToken): Promise; - isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; - getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; + isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; + getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; @@ -365,6 +377,7 @@ export interface InstallExtensionEvent { readonly source: URI | IGalleryExtension; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export interface InstallExtensionResult { @@ -376,12 +389,14 @@ export interface InstallExtensionResult { readonly context?: IStringDictionary; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export interface UninstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export interface DidUninstallExtensionEvent { @@ -389,6 +404,7 @@ export interface DidUninstallExtensionEvent { readonly error?: string; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export enum ExtensionManagementErrorCode { @@ -443,6 +459,7 @@ export class ExtensionGalleryError extends Error { export type InstallOptions = { isBuiltin?: boolean; + isWorkspaceScoped?: boolean; isMachineScoped?: boolean; isApplicationScoped?: boolean; pinned?: boolean; @@ -453,16 +470,17 @@ export type InstallOptions = { donotVerifySignature?: boolean; operation?: InstallOperation; profileLocation?: URI; + installOnlyNewlyAddedFromExtensionPack?: boolean; + productVersion?: IProductVersion; /** * Context passed through to InstallExtensionResult */ context?: IStringDictionary; }; -export type InstallVSIXOptions = InstallOptions & { installOnlyNewlyAddedFromExtensionPack?: boolean }; export type UninstallOptions = { readonly donotIncludePack?: boolean; readonly donotCheckDependents?: boolean; readonly versionOnly?: boolean; readonly remove?: boolean; readonly profileLocation?: URI }; export interface IExtensionManagementParticipant { - postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions | InstallVSIXOptions, token: CancellationToken): Promise; + postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions, token: CancellationToken): Promise; postUninstall(local: ILocalExtension, options: UninstallOptions, token: CancellationToken): Promise; } @@ -481,7 +499,7 @@ export interface IExtensionManagementService { zip(extension: ILocalExtension): Promise; unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; - install(vsix: URI, options?: InstallVSIXOptions): Promise; + install(vsix: URI, options?: InstallOptions): Promise; canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise; @@ -490,7 +508,7 @@ export interface IExtensionManagementService { uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; - getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; diff --git a/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index d4f15349aad..6c3e289db7d 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -139,7 +139,7 @@ export class ExtensionManagementChannel implements IServerChannel { return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); } case 'getInstalled': { - const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer)); + const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer), args[2]); return extensions.map(e => transformOutgoingExtension(e, uriTransformer)); } case 'toggleAppliationScope': { @@ -237,7 +237,7 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('unzip', [zipLocation])); } - install(vsix: URI, options?: InstallVSIXOptions): Promise { + install(vsix: URI, options?: InstallOptions): Promise { return Promise.resolve(this.channel.call('install', [vsix, options])).then(local => transformIncomingExtension(local, null)); } @@ -264,6 +264,9 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt } uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { + if (extension.isWorkspaceScoped) { + throw new Error('Cannot uninstall a workspace extension'); + } return Promise.resolve(this.channel.call('uninstall', [extension, options])); } @@ -271,8 +274,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('reinstallFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } - getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { - return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource])) + getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI, productVersion?: IProductVersion): Promise { + return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource, productVersion])) .then(extensions => extensions.map(extension => transformIncomingExtension(extension, null))); } diff --git a/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 4fcf403d999..fddaf329af0 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -83,7 +83,7 @@ export interface IExtensionsProfileScannerService { readonly onDidRemoveExtensions: Event; scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise; - addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; + addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise; updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise; } @@ -120,18 +120,22 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable return this.withProfileExtensions(profileLocation, undefined, options); } - async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise { + async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; const extensionsToAdd: IScannedProfileExtension[] = []; try { await this.withProfileExtensions(profileLocation, existingExtensions => { const result: IScannedProfileExtension[] = []; - for (const existing of existingExtensions) { - if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { - // Remove the existing extension with different version - extensionsToRemove.push(existing); - } else { - result.push(existing); + if (keepExistingVersions) { + result.push(...existingExtensions); + } else { + for (const existing of existingExtensions) { + if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { + // Remove the existing extension with different version + extensionsToRemove.push(existing); + } else { + result.push(existing); + } } } for (const [extension, metadata] of extensions) { diff --git a/code/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/code/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 4ac28d17b5f..0b64c8b9b33 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -22,7 +22,7 @@ import { isEmptyObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; @@ -108,6 +108,7 @@ export type ScanOptions = { readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; + readonly productVersion?: IProductVersion; }; export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); @@ -126,6 +127,7 @@ export interface IExtensionsScannerService { scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise; scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; + scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise; scanMetadata(extensionLocation: URI): Promise; updateMetadata(extensionLocation: URI, metadata: Partial): Promise; @@ -195,7 +197,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions); + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { @@ -217,7 +219,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -233,7 +235,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,11 +247,20 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } + async scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise { + const extensions: IRelaxedScannedExtension[] = []; + await Promise.all(extensionLocations.map(async extensionLocation => { + const scannedExtensions = await this.scanOneOrMultipleExtensions(extensionLocation, extensionType, scanOptions); + extensions.push(...scannedExtensions); + })); + return this.applyScanOptions(extensions, extensionType, scanOptions, true); + } + async scanMetadata(extensionLocation: URI): Promise { const manifestLocation = joinPath(extensionLocation, 'package.json'); const content = (await this.fileService.readFile(manifestLocation)).value.toString(); @@ -392,7 +403,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -422,7 +433,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -436,7 +447,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -451,8 +462,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem type, excludeObsolete, validate, - this.productService.version, - this.productService.date, + productVersion.version, + productVersion.date, this.productService.commit, !this.environmentService.isBuilt, language, @@ -472,6 +483,13 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return undefined; } + private getProductVersion(): IProductVersion { + return { + version: this.productService.version, + date: this.productService.date, + }; + } + } export class ExtensionScannerInput { diff --git a/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts b/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts index 7864019ad83..98f5a2194f9 100644 --- a/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts +++ b/code/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts @@ -7,10 +7,11 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { AbstractExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; +import { AbstractExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IFileService } from 'vs/platform/files/common/files'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class ExtensionsProfileScannerService extends AbstractExtensionsProfileScannerService { constructor( @@ -24,3 +25,5 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); } } + +registerSingleton(IExtensionsProfileScannerService, ExtensionsProfileScannerService, InstantiationType.Delayed); diff --git a/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 217d7720d15..58ce27065ed 100644 --- a/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -28,7 +28,8 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, - Metadata, InstallVSIXOptions + Metadata, InstallOptions, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -128,8 +129,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } - getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { - return this.extensionsScanner.scanExtensions(type ?? null, profileLocation); + getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { + return this.extensionsScanner.scanExtensions(type ?? null, profileLocation, productVersion); } scanAllUserInstalledExtensions(): Promise { @@ -140,7 +141,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.scanUserExtensionAtLocation(location); } - async install(vsix: URI, options: InstallVSIXOptions = {}): Promise { + async install(vsix: URI, options: InstallOptions = {}): Promise { this.logService.trace('ExtensionManagementService#install', vsix.toString()); const { location, cleanup } = await this.downloadVsix(vsix); @@ -172,14 +173,14 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi if (!local || !local.manifest.name || !local.manifest.version) { throw new Error(`Cannot find a valid extension from the location ${location.toString()}`); } - await this.addExtensionsToProfile([[local, undefined]], profileLocation); + await this.addExtensionsToProfile([[local, { source: 'resource' }]], profileLocation); this.logService.info('Successfully installed extension', local.identifier.id, profileLocation.toString()); return local; } async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { this.logService.trace('ExtensionManagementService#installExtensionsFromProfile', extensions, fromProfileLocation.toString(), toProfileLocation.toString()); - const extensionsToInstall = (await this.extensionsScanner.scanExtensions(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); + const extensionsToInstall = (await this.getInstalled(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); if (extensionsToInstall.length) { const metadata = await Promise.all(extensionsToInstall.map(e => this.extensionsScanner.scanMetadata(e, fromProfileLocation))); await this.addExtensionsToProfile(extensionsToInstall.map((e, index) => [e, metadata[index]]), toProfileLocation); @@ -236,7 +237,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation); + return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } markAsUninstalled(...extensions: IExtension[]): Promise { @@ -333,7 +334,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } if (added) { - const extensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, added.profileLocation); + const extensions = await this.getInstalled(ExtensionType.User, added.profileLocation); const addedExtensions = extensions.filter(e => added.extensions.some(identifier => areSameExtensions(identifier, e.identifier))); this._onDidInstallExtensions.fire(addedExtensions.map(local => { this.logService.info('Extensions added from another source', local.identifier.id, added.profileLocation.toString()); @@ -449,8 +450,8 @@ export class ExtensionsScanner extends Disposable { await this.removeUninstalledExtensions(); } - async scanExtensions(type: ExtensionType | null, profileLocation: URI): Promise { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation }; + async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { + const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); @@ -613,8 +614,8 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(extension.location, extension.type, toProfileLocation); } - async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation); + async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, productVersion: IProductVersion): Promise { + const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation, productVersion); const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions .filter(e => !e.isApplicationScoped) /* remove application scoped extensions */ .map(async e => ([e, await this.scanMetadata(e, fromProfileLocation)]))); @@ -714,6 +715,8 @@ export class ExtensionsScanner extends Disposable { installedTimestamp: extension.metadata?.installedTimestamp, updated: !!extension.metadata?.updated, pinned: !!extension.metadata?.pinned, + isWorkspaceScoped: false, + source: extension.metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'vsix') }; } @@ -819,7 +822,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { let installed; try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation); + installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); } catch (error) { throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } @@ -905,7 +908,8 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), preRelease: isBoolean(this.options.preRelease) ? this.options.preRelease - : this.options.installPreReleaseVersion || this.gallery.properties.isPreReleaseVersion || existingExtension?.preRelease + : this.options.installPreReleaseVersion || this.gallery.properties.isPreReleaseVersion || existingExtension?.preRelease, + source: 'gallery', }; if (existingExtension?.manifest.version === this.gallery.version) { @@ -969,7 +973,7 @@ class InstallVSIXTask extends InstallExtensionTask { protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { const extensionKey = new ExtensionKey(this.identifier, this.manifest.version); - const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation); + const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); const metadata: Metadata = { isApplicationScoped: this.options.isApplicationScoped || existing?.isApplicationScoped, @@ -977,6 +981,7 @@ class InstallVSIXTask extends InstallExtensionTask { isBuiltin: this.options.isBuiltin || existing?.isBuiltin, installedTimestamp: Date.now(), pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existing?.pinned), + source: 'vsix', }; if (existing) { diff --git a/code/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/code/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index 66d1ffc28e2..05d218b50b5 100644 --- a/code/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/code/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -6,6 +6,7 @@ import { getErrorMessage } from 'vs/base/common/errors'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IExtensionSignatureVerificationService = createDecorator('IExtensionSignatureVerificationService'); @@ -47,7 +48,8 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur private moduleLoadingPromise: Promise | undefined; constructor( - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { } private vsceSign(): Promise { @@ -75,6 +77,35 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur return false; } - return module.verify(vsixFilePath, signatureArchiveFilePath, verbose); + const startTime = new Date().getTime(); + let verified: boolean | undefined; + let error: ExtensionSignatureVerificationError | undefined; + + try { + verified = await module.verify(vsixFilePath, signatureArchiveFilePath, verbose); + return verified; + } catch (e) { + error = e; + throw e; + } finally { + const duration = new Date().getTime() - startTime; + type ExtensionSignatureVerificationClassification = { + owner: 'sandy081'; + comment: 'Extension signature verification event'; + duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken to verify the signature' }; + verified?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'verified status when succeeded' }; + error?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code when failed' }; + }; + type ExtensionSignatureVerificationEvent = { + duration: number; + verified?: boolean; + error?: string; + }; + this.telemetryService.publicLog2('extensionsignature:verification', { + duration, + verified, + error: error ? (error.code ?? 'unknown') : undefined, + }); + } } } diff --git a/code/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/code/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index 1767e8931cb..7c40a4af114 100644 --- a/code/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/code/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -102,7 +102,7 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { engines: { vscode: '*' }, }, extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource }, + { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, extensionDownloader, new TestExtensionsScanner(), uriIdentityService, diff --git a/code/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/code/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index 118cbf8d5ec..07639a7e7b6 100644 --- a/code/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/code/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const enum RecommendationSource { @@ -43,6 +44,6 @@ export interface IExtensionRecommendationNotificationService { hasToIgnoreRecommendationNotifications(): boolean; promptImportantExtensionsInstallNotification(recommendations: IExtensionRecommendations): Promise; - promptWorkspaceRecommendations(recommendations: string[]): Promise; + promptWorkspaceRecommendations(recommendations: Array): Promise; } diff --git a/code/src/vs/platform/extensions/common/extensions.ts b/code/src/vs/platform/extensions/common/extensions.ts index 62977ee07e3..331aba1b55f 100644 --- a/code/src/vs/platform/extensions/common/extensions.ts +++ b/code/src/vs/platform/extensions/common/extensions.ts @@ -255,6 +255,7 @@ export const EXTENSION_CATEGORIES = [ 'Testing', 'Themes', 'Visualization', + 'AI', 'Chat', 'Other', ]; diff --git a/code/src/vs/platform/externalTerminal/node/externalTerminalService.ts b/code/src/vs/platform/externalTerminal/node/externalTerminalService.ts index a8df823266a..5086c95a802 100644 --- a/code/src/vs/platform/externalTerminal/node/externalTerminalService.ts +++ b/code/src/vs/platform/externalTerminal/node/externalTerminalService.ts @@ -80,8 +80,7 @@ export class WindowsExternalTerminalService extends ExternalTerminalService impl return new Promise((resolve, reject) => { const title = `"${dir} - ${TERMINAL_TITLE}"`; - const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code - + const command = `"${args.join('" "')}" & pause`; // use '|' to only pause on non-zero exit code // merge environment variables into a copy of the process.env const env = Object.assign({}, getSanitizedEnvironment(process), envVars); @@ -110,7 +109,7 @@ export class WindowsExternalTerminalService extends ExternalTerminalService impl cmdArgs = ['-d', '.', exec, '/c', command]; } else { spawnExec = WindowsExternalTerminalService.CMD; - cmdArgs = ['/c', 'start', title, '/wait', exec, '/c', command]; + cmdArgs = ['/c', 'start', title, '/wait', exec, '/c', `"${command}"`]; } const cmd = cp.spawn(spawnExec, cmdArgs, options); diff --git a/code/src/vs/platform/files/common/files.ts b/code/src/vs/platform/files/common/files.ts index 0bc285f082e..e8bcce418a8 100644 --- a/code/src/vs/platform/files/common/files.ts +++ b/code/src/vs/platform/files/common/files.ts @@ -243,14 +243,6 @@ export interface IFileService { */ createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation): IFileSystemWatcher; - /** - * Allows to start a watcher that reports file/folder change events on the provided resource. - * - * The watcher runs correlated and thus, file events will be reported on the returned - * `IFileSystemWatcher` and not on the generic `IFileService.onDidFilesChange` event. - */ - watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher; - /** * Allows to start a watcher that reports file/folder change events on the provided resource. * diff --git a/code/src/vs/platform/files/common/watcher.ts b/code/src/vs/platform/files/common/watcher.ts index ae97f833078..3aeba5d80ad 100644 --- a/code/src/vs/platform/files/common/watcher.ts +++ b/code/src/vs/platform/files/common/watcher.ts @@ -43,6 +43,14 @@ interface IWatchRequest { readonly correlationId?: number; } +export interface IWatchRequestWithCorrelation extends IWatchRequest { + readonly correlationId: number; +} + +export function isWatchRequestWithCorrelation(request: IWatchRequest): request is IWatchRequestWithCorrelation { + return typeof request.correlationId === 'number'; +} + export interface INonRecursiveWatchRequest extends IWatchRequest { /** @@ -71,7 +79,7 @@ export function isRecursiveWatchRequest(request: IWatchRequest): request is IRec export type IUniversalWatchRequest = IRecursiveWatchRequest | INonRecursiveWatchRequest; -interface IWatcher { +export interface IWatcher { /** * A normalized file change event from the raw events diff --git a/code/src/vs/platform/files/electron-main/diskFileSystemProviderServer.ts b/code/src/vs/platform/files/electron-main/diskFileSystemProviderServer.ts index fdc7e6a9117..64cbede2fd1 100644 --- a/code/src/vs/platform/files/electron-main/diskFileSystemProviderServer.ts +++ b/code/src/vs/platform/files/electron-main/diskFileSystemProviderServer.ts @@ -16,6 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { AbstractDiskFileSystemProviderChannel, AbstractSessionFileWatcher, ISessionFileWatcher } from 'vs/platform/files/node/diskFileSystemProviderServer'; import { DefaultURITransformer, IURITransformer } from 'vs/base/common/uriIpc'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; export class DiskFileSystemProviderChannel extends AbstractDiskFileSystemProviderChannel { @@ -47,7 +48,7 @@ export class DiskFileSystemProviderChannel extends AbstractDiskFileSystemProvide try { await shell.trashItem(filePath); } catch (error) { - throw createFileSystemProviderError(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)), FileSystemProviderErrorCode.Unknown); + throw createFileSystemProviderError(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin ({1})", basename(filePath), toErrorMessage(error)) : localize('trashFailed', "Failed to move '{0}' to the trash ({1})", basename(filePath), toErrorMessage(error)), FileSystemProviderErrorCode.Unknown); } } diff --git a/code/src/vs/platform/files/node/watcher/baseWatcher.ts b/code/src/vs/platform/files/node/watcher/baseWatcher.ts new file mode 100644 index 00000000000..9b9c35ae6c3 --- /dev/null +++ b/code/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { watchFile, unwatchFile, Stats } from 'fs'; +import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ILogMessage, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, isWatchRequestWithCorrelation } from 'vs/platform/files/common/watcher'; +import { Emitter, Event } from 'vs/base/common/event'; +import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; + +export abstract class BaseWatcher extends Disposable implements IWatcher { + + protected readonly _onDidChangeFile = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChangeFile.event; + + protected readonly _onDidLogMessage = this._register(new Emitter()); + readonly onDidLogMessage = this._onDidLogMessage.event; + + protected readonly _onDidWatchFail = this._register(new Emitter()); + private readonly onDidWatchFail = this._onDidWatchFail.event; + + private readonly allNonCorrelatedWatchRequests = new Set(); + private readonly allCorrelatedWatchRequests = new Map(); + + private readonly suspendedWatchRequests = this._register(new DisposableMap()); + + protected readonly suspendedWatchRequestPollingInterval: number = 5007; // node.js default + + constructor() { + super(); + + this._register(this.onDidWatchFail(request => this.handleDidWatchFail(request))); + } + + private handleDidWatchFail(request: IUniversalWatchRequest): void { + if (!this.isCorrelated(request)) { + + // For now, limit failed watch monitoring to requests with a correlationId + // to experiment with this feature in a controlled way. Monitoring requests + // requires us to install polling watchers (via `fs.watchFile()`) and thus + // should be used sparingly. + + return; + } + + this.suspendWatchRequest(request); + } + + protected isCorrelated(request: IUniversalWatchRequest): request is IWatchRequestWithCorrelation { + return isWatchRequestWithCorrelation(request); + } + + async watch(requests: IUniversalWatchRequest[]): Promise { + this.allCorrelatedWatchRequests.clear(); + this.allNonCorrelatedWatchRequests.clear(); + + // Figure out correlated vs. non-correlated requests + for (const request of requests) { + if (this.isCorrelated(request)) { + this.allCorrelatedWatchRequests.set(request.correlationId, request); + } else { + this.allNonCorrelatedWatchRequests.add(request); + } + } + + // Remove all suspended correlated watch requests that are no longer watched + for (const [correlationId] of this.suspendedWatchRequests) { + if (!this.allCorrelatedWatchRequests.has(correlationId)) { + this.suspendedWatchRequests.deleteAndDispose(correlationId); + } + } + + return this.updateWatchers(); + } + + private updateWatchers(): Promise { + return this.doWatch([ + ...this.allNonCorrelatedWatchRequests, + ...Array.from(this.allCorrelatedWatchRequests.values()).filter(request => !this.suspendedWatchRequests.has(request.correlationId)) + ]); + } + + private suspendWatchRequest(request: IWatchRequestWithCorrelation): void { + if (this.suspendedWatchRequests.has(request.correlationId)) { + return; // already suspended + } + + const disposables = new DisposableStore(); + this.suspendedWatchRequests.set(request.correlationId, disposables); + + this.monitorSuspendedWatchRequest(request, disposables); + + this.updateWatchers(); + } + + private resumeWatchRequest(request: IWatchRequestWithCorrelation): void { + this.suspendedWatchRequests.deleteAndDispose(request.correlationId); + + this.updateWatchers(); + } + + private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore) { + const resource = URI.file(request.path); + const that = this; + + let pathNotFound = false; + + const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => { + if (disposables.isDisposed) { + return; // return early if already disposed + } + + const currentPathNotFound = this.isPathNotFound(curr); + const previousPathNotFound = this.isPathNotFound(prev); + const oldPathNotFound = pathNotFound; + pathNotFound = currentPathNotFound; + + // Watch path created: resume watching request + if (!currentPathNotFound && (previousPathNotFound || oldPathNotFound)) { + this.trace(`fs.watchFile() detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`); + + // Emit as event + const event: IFileChange = { resource, type: FileChangeType.ADDED, cId: request.correlationId }; + that._onDidChangeFile.fire([event]); + this.traceEvent(event, request); + + // Resume watching + this.resumeWatchRequest(request); + } + }; + + this.trace(`starting fs.watchFile() on ${request.path} (correlationId: ${request.correlationId})`); + try { + watchFile(request.path, { persistent: false, interval: this.suspendedWatchRequestPollingInterval }, watchFileCallback); + } catch (error) { + this.warn(`fs.watchFile() failed with error ${error} on path ${request.path} (correlationId: ${request.correlationId})`); + } + + disposables.add(toDisposable(() => { + this.trace(`stopping fs.watchFile() on ${request.path} (correlationId: ${request.correlationId})`); + + try { + unwatchFile(request.path, watchFileCallback); + } catch (error) { + this.warn(`fs.unwatchFile() failed with error ${error} on path ${request.path} (correlationId: ${request.correlationId})`); + } + })); + } + + private isPathNotFound(stats: Stats): boolean { + return stats.ctimeMs === 0 && stats.ino === 0; + } + + async stop(): Promise { + this.suspendedWatchRequests.clearAndDisposeAll(); + } + + protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void { + const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`; + this.trace(typeof request.correlationId === 'number' ? `${traceMsg} (correlationId: ${request.correlationId})` : traceMsg); + } + + protected requestToString(request: IUniversalWatchRequest): string { + return `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : ''}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : ''}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : ''})`; + } + + protected abstract doWatch(requests: IUniversalWatchRequest[]): Promise; + + protected abstract trace(message: string): void; + protected abstract warn(message: string): void; + + abstract onDidError: Event; + abstract setVerboseLogging(enabled: boolean): Promise; +} diff --git a/code/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts b/code/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts index 11eb6b8a109..2a662eb7e05 100644 --- a/code/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts +++ b/code/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts @@ -21,6 +21,6 @@ export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient { } protected override createWatcher(disposables: DisposableStore): INonRecursiveWatcher { - return disposables.add(new NodeJSWatcher()); + return disposables.add(new NodeJSWatcher()) satisfies INonRecursiveWatcher; } } diff --git a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index dac55a138c5..197c975a465 100644 --- a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { patternsEquals } from 'vs/base/common/glob'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher'; import { isLinux } from 'vs/base/common/platform'; -import { IFileChange } from 'vs/platform/files/common/files'; -import { ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; +import { INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; +import { isEqual } from 'vs/base/common/extpath'; export interface INodeJSWatcherInstance { @@ -24,90 +24,101 @@ export interface INodeJSWatcherInstance { readonly request: INonRecursiveWatchRequest; } -export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { - - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private readonly _onDidLogMessage = this._register(new Emitter()); - readonly onDidLogMessage = this._onDidLogMessage.event; +export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { readonly onDidError = Event.None; - protected readonly watchers = new Map(); + protected readonly watchers = new Set(); private verboseLogging = false; - async watch(requests: INonRecursiveWatchRequest[]): Promise { + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { // Figure out duplicates to remove from the requests - const normalizedRequests = this.normalizeRequests(requests); + requests = this.removeDuplicateRequests(requests); - // Gather paths that we should start watching - const requestsToStartWatching = normalizedRequests.filter(request => { - const watcher = this.watchers.get(request.path); - if (!watcher) { - return true; // not yet watching that path + // Figure out which watchers to start and which to stop + const requestsToStart: INonRecursiveWatchRequest[] = []; + const watchersToStop = new Set(Array.from(this.watchers)); + for (const request of requests) { + const watcher = this.findWatcher(request); + if (watcher && patternsEquals(watcher.request.excludes, request.excludes) && patternsEquals(watcher.request.includes, request.includes)) { + watchersToStop.delete(watcher); // keep watcher + } else { + requestsToStart.push(request); // start watching } - // Re-watch path if excludes or includes have changed - return !patternsEquals(watcher.request.excludes, request.excludes) || !patternsEquals(watcher.request.includes, request.includes); - }); - - // Gather paths that we should stop watching - const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { - return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && patternsEquals(normalizedRequest.excludes, request.excludes) && patternsEquals(normalizedRequest.includes, request.includes)); - }).map(({ request }) => request.path); + } // Logging - if (requestsToStartWatching.length) { - this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : ''}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : ''}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : ''})`).join(',')}`); + if (requestsToStart.length) { + this.trace(`Request to start watching: ${requestsToStart.map(request => this.requestToString(request)).join(',')}`); } - if (pathsToStopWatching.length) { - this.trace(`Request to stop watching: ${pathsToStopWatching.join(',')}`); + if (watchersToStop.size) { + this.trace(`Request to stop watching: ${Array.from(watchersToStop).map(watcher => this.requestToString(watcher.request)).join(',')}`); } // Stop watching as instructed - for (const pathToStopWatching of pathsToStopWatching) { - this.stopWatching(pathToStopWatching); + for (const watcher of watchersToStop) { + this.stopWatching(watcher); } // Start watching as instructed - for (const request of requestsToStartWatching) { + for (const request of requestsToStart) { this.startWatching(request); } } + private findWatcher(request: INonRecursiveWatchRequest): INodeJSWatcherInstance | undefined { + for (const watcher of this.watchers) { + + // Requests or watchers with correlation always match on that + if (typeof request.correlationId === 'number' || typeof watcher.request.correlationId === 'number') { + if (watcher.request.correlationId === request.correlationId) { + return watcher; + } + } + + // Non-correlated requests or watchers match on path + else { + if (isEqual(watcher.request.path, request.path, !isLinux /* ignorecase */)) { + return watcher; + } + } + } + + return undefined; + } + private startWatching(request: INonRecursiveWatchRequest): void { // Start via node.js lib - const instance = new NodeJSFileWatcherLibrary(request, changes => this._onDidChangeFile.fire(changes), msg => this._onDidLogMessage.fire(msg), this.verboseLogging); + const instance = new NodeJSFileWatcherLibrary(request, changes => this._onDidChangeFile.fire(changes), () => this._onDidWatchFail.fire(request), msg => this._onDidLogMessage.fire(msg), this.verboseLogging); // Remember as watcher instance const watcher: INodeJSWatcherInstance = { request, instance }; - this.watchers.set(request.path, watcher); + this.watchers.add(watcher); } - async stop(): Promise { - for (const [path] of this.watchers) { - this.stopWatching(path); - } + override async stop(): Promise { + await super.stop(); - this.watchers.clear(); + for (const watcher of this.watchers) { + this.stopWatching(watcher); + } } - private stopWatching(path: string): void { - const watcher = this.watchers.get(path); - if (watcher) { - this.watchers.delete(path); + private stopWatching(watcher: INodeJSWatcherInstance): void { + this.trace(`stopping file watcher`, watcher); - watcher.instance.dispose(); - } + this.watchers.delete(watcher); + + watcher.instance.dispose(); } - private normalizeRequests(requests: INonRecursiveWatchRequest[]): INonRecursiveWatchRequest[] { + private removeDuplicateRequests(requests: INonRecursiveWatchRequest[]): INonRecursiveWatchRequest[] { const mapCorrelationtoRequests = new Map>(); // Ignore requests for the same paths that have the same correlation @@ -120,6 +131,10 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation); } + if (requestsForCorrelation.has(path)) { + this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`); + } + requestsForCorrelation.set(path, request); } @@ -129,18 +144,22 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { async setVerboseLogging(enabled: boolean): Promise { this.verboseLogging = enabled; - for (const [, watcher] of this.watchers) { + for (const watcher of this.watchers) { watcher.instance.setVerboseLogging(enabled); } } - private trace(message: string): void { + protected trace(message: string, watcher?: INodeJSWatcherInstance): void { if (this.verboseLogging) { - this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message) }); + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) }); } } + protected warn(message: string): void { + this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message) }); + } + private toMessage(message: string, watcher?: INodeJSWatcherInstance): string { - return watcher ? `[File Watcher (node.js)] ${message} (path: ${watcher.request.path})` : `[File Watcher (node.js)] ${message}`; + return watcher ? `[File Watcher (node.js)] ${message} (${this.requestToString(watcher.request)})` : `[File Watcher (node.js)] ${message}`; } } diff --git a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 8f0f7d6a87f..ba9edc88f0f 100644 --- a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -57,9 +57,10 @@ export class NodeJSFileWatcherLibrary extends Disposable { readonly ready = this.watch(); constructor( - private request: INonRecursiveWatchRequest, - private onDidFilesChange: (changes: IFileChange[]) => void, - private onLogMessage?: (msg: ILogMessage) => void, + private readonly request: INonRecursiveWatchRequest, + private readonly onDidFilesChange: (changes: IFileChange[]) => void, + private readonly onDidWatchFail?: () => void, + private readonly onLogMessage?: (msg: ILogMessage) => void, private verboseLogging?: boolean ) { super(); @@ -73,16 +74,21 @@ export class NodeJSFileWatcherLibrary extends Disposable { return; } - // Watch via node.js const stat = await Promises.stat(realPath); - this._register(await this.doWatch(realPath, stat.isDirectory())); + if (this.cts.token.isCancellationRequested) { + return; + } + + this._register(await this.doWatch(realPath, stat.isDirectory())); } catch (error) { if (error.code !== 'ENOENT') { this.error(error); } else { - this.trace(error); + this.trace(`ignoring a path for watching who's stat info failed to resolve: ${this.request.path} (error: ${error})`); } + + this.onDidWatchFail?.(); } } @@ -97,7 +103,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { // Second check for casing difference // Note: this will be a no-op on Linux platforms if (request.path === realPath) { - realPath = await realcase(request.path) ?? request.path; + realPath = await realcase(request.path, this.cts.token) ?? request.path; } // Correct watch path as needed @@ -164,9 +170,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { watcher.on('error', (code: number, signal: string) => { this.error(`Failed to watch ${path} for changes using fs.watch() (${code}, ${signal})`); - // The watcher is no longer functional reliably - // so we go ahead and dispose it - this.dispose(); + this.onDidWatchFail?.(); }); watcher.on('change', (type, raw) => { @@ -224,15 +228,8 @@ export class NodeJSFileWatcherLibrary extends Disposable { // file watching specifically we want to handle // the atomic-write cases where the file is being // deleted and recreated with different contents. - // - // Same as with recursive watching, we do not - // emit a delete event in this case. if (changedFileName === pathBasename && !await Promises.exists(path)) { - this.warn('Watcher shutdown because watched path got deleted'); - - // The watcher is no longer functional reliably - // so we go ahead and dispose it - this.dispose(); + this.onWatchedPathDeleted(requestResource); return; } @@ -326,16 +323,9 @@ export class NodeJSFileWatcherLibrary extends Disposable { disposables.add(await this.doWatch(path, false)); } - // File seems to be really gone, so emit a deleted event and dispose + // File seems to be really gone, so emit a deleted and failed event else { - this.onFileChange({ resource: requestResource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); - - // Important to flush the event delivery - // before disposing the watcher, otherwise - // we will loose this event. - this.fileChangesAggregator.flush(); - - this.dispose(); + this.onWatchedPathDeleted(requestResource); } }, NodeJSFileWatcherLibrary.FILE_DELETE_HANDLER_DELAY); @@ -352,9 +342,11 @@ export class NodeJSFileWatcherLibrary extends Disposable { } }); } catch (error) { - if (await Promises.exists(path) && !cts.token.isCancellationRequested) { + if (!cts.token.isCancellationRequested) { this.error(`Failed to watch ${path} for changes using fs.watch() (${error.toString()})`); } + + this.onDidWatchFail?.(); } return toDisposable(() => { @@ -363,6 +355,16 @@ export class NodeJSFileWatcherLibrary extends Disposable { }); } + private onWatchedPathDeleted(resource: URI): void { + this.warn('Watcher shutdown because watched path got deleted'); + + // Emit events and flush in case the watcher gets disposed + this.onFileChange({ resource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); + this.fileChangesAggregator.flush(); + + this.onDidWatchFail?.(); + } + private onFileChange(event: IFileChange, skipIncludeExcludeChecks = false): void { if (this.cts.token.isCancellationRequested) { return; @@ -454,8 +456,6 @@ export class NodeJSFileWatcherLibrary extends Disposable { } override dispose(): void { - this.trace(`stopping file watcher on ${this.request.path}`); - this.cts.dispose(true); super.dispose(); diff --git a/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index d1b978043ed..3ba2370fbe2 100644 --- a/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/code/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -11,9 +11,9 @@ import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } fro import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; -import { randomPath } from 'vs/base/common/extpath'; +import { randomPath, isEqual } from 'vs/base/common/extpath'; import { GLOBSTAR, ParsedPattern, patternsEquals } from 'vs/base/common/glob'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { normalizeNFC } from 'vs/base/common/normalization'; import { dirname, normalize } from 'vs/base/common/path'; @@ -21,7 +21,7 @@ import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; +import { coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; export interface IParcelWatcherInstance { @@ -58,7 +58,7 @@ export interface IParcelWatcherInstance { stop(): Promise; } -export class ParcelWatcher extends Disposable implements IRecursiveWatcher { +export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map( [ @@ -70,16 +70,10 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private readonly _onDidLogMessage = this._register(new Emitter()); - readonly onDidLogMessage = this._onDidLogMessage.event; - private readonly _onDidError = this._register(new Emitter()); readonly onDidError = this._onDidError.event; - protected readonly watchers = new Map(); + protected readonly watchers = new Set(); // A delay for collecting file changes from Parcel // before collecting them for coalescing and emitting. @@ -120,50 +114,39 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { process.on('unhandledRejection', error => this.onUnexpectedError(error)); } - async watch(requests: IRecursiveWatchRequest[]): Promise { + protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { // Figure out duplicates to remove from the requests - const normalizedRequests = this.normalizeRequests(requests); + requests = this.removeDuplicateRequests(requests); - // Gather paths that we should start watching - const requestsToStartWatching = normalizedRequests.filter(request => { - const watcher = this.watchers.get(request.path); - if (!watcher) { - return true; // not yet watching that path + // Figure out which watchers to start and which to stop + const requestsToStart: IRecursiveWatchRequest[] = []; + const watchersToStop = new Set(Array.from(this.watchers)); + for (const request of requests) { + const watcher = this.findWatcher(request); + if (watcher && patternsEquals(watcher.request.excludes, request.excludes) && patternsEquals(watcher.request.includes, request.includes) && watcher.request.pollingInterval === request.pollingInterval) { + watchersToStop.delete(watcher); // keep watcher + } else { + requestsToStart.push(request); // start watching } - - // Re-watch path if excludes/includes have changed or polling interval - return !patternsEquals(watcher.request.excludes, request.excludes) || !patternsEquals(watcher.request.includes, request.includes) || watcher.request.pollingInterval !== request.pollingInterval; - }); - - // Gather paths that we should stop watching - const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { - return !normalizedRequests.find(normalizedRequest => { - return normalizedRequest.path === request.path && - patternsEquals(normalizedRequest.excludes, request.excludes) && - patternsEquals(normalizedRequest.includes, request.includes) && - normalizedRequest.pollingInterval === request.pollingInterval; - - }); - }).map(({ request }) => request.path); + } // Logging - - if (requestsToStartWatching.length) { - this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : ''}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : ''}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : ''})`).join(',')}`); + if (requestsToStart.length) { + this.trace(`Request to start watching: ${requestsToStart.map(request => this.requestToString(request)).join(',')}`); } - if (pathsToStopWatching.length) { - this.trace(`Request to stop watching: ${pathsToStopWatching.join(',')}`); + if (watchersToStop.size) { + this.trace(`Request to stop watching: ${Array.from(watchersToStop).map(watcher => this.requestToString(watcher.request)).join(',')}`); } // Stop watching as instructed - for (const pathToStopWatching of pathsToStopWatching) { - await this.stopWatching(pathToStopWatching); + for (const watcher of watchersToStop) { + await this.stopWatching(watcher); } // Start watching as instructed - for (const request of requestsToStartWatching) { + for (const request of requestsToStart) { if (request.pollingInterval) { this.startPolling(request, request.pollingInterval); } else { @@ -172,6 +155,27 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } } + private findWatcher(request: IRecursiveWatchRequest): IParcelWatcherInstance | undefined { + for (const watcher of this.watchers) { + + // Requests or watchers with correlation always match on that + if (typeof request.correlationId === 'number' || typeof watcher.request.correlationId === 'number') { + if (watcher.request.correlationId === request.correlationId) { + return watcher; + } + } + + // Non-correlated requests or watchers match on path + else { + if (isEqual(watcher.request.path, request.path, !isLinux /* ignorecase */)) { + return watcher; + } + } + } + + return undefined; + } + private startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): void { const cts = new CancellationTokenSource(); @@ -196,7 +200,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { unlinkSync(snapshotFile); } }; - this.watchers.set(request.path, watcher); + this.watchers.add(watcher); // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); @@ -267,7 +271,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { await watcherInstance?.unsubscribe(); } }; - this.watchers.set(request.path, watcher); + this.watchers.add(watcher); // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); @@ -301,6 +305,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { this.onUnexpectedError(error, watcher); instance.complete(undefined); + + this._onDidWatchFail.fire(request); }); } @@ -370,8 +376,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Logging if (this.verboseLogging) { for (const event of events) { - const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`; - this.trace(typeof watcher.request.correlationId === 'number' ? `${traceMsg} (correlationId: ${watcher.request.correlationId})` : traceMsg); + this.traceEvent(event, watcher.request); } } @@ -383,7 +388,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { this.warn(`started ignoring events due to too many file change events at once (incoming: ${events.length}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); } else { if (this.throttledFileChangesEmitter.pending > 0) { - this.trace(`started throttling events due to large amount of file change events at once (pending: ${this.throttledFileChangesEmitter.pending}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); + this.trace(`started throttling events due to large amount of file change events at once (pending: ${this.throttledFileChangesEmitter.pending}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`, watcher); } } } @@ -446,18 +451,25 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { let rootDeleted = false; for (const event of events) { - if (event.type === FileChangeType.DELETED && event.resource.fsPath === watcher.request.path) { + rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux); + + if (rootDeleted && !this.isCorrelated(watcher.request)) { // Explicitly exclude changes to root if we have any // to avoid VS Code closing all opened editors which // can happen e.g. in case of network connectivity // issues // (https://github.com/microsoft/vscode/issues/136673) + // + // Update 2024: with the new correlated events, we + // really do not want to skip over file events any + // more, so we only ignore this event for non-correlated + // watch requests. - rootDeleted = true; - } else { - filteredEvents.push(event); + continue; } + + filteredEvents.push(event); } return { events: filteredEvents, rootDeleted }; @@ -466,8 +478,25 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { private onWatchedPathDeleted(watcher: IParcelWatcherInstance): void { this.warn('Watcher shutdown because watched path got deleted', watcher); + this._onDidWatchFail.fire(watcher.request); + + // Do monitoring of the request path parent unless this request + // can be handled via suspend/resume in the super class + // + // TODO@bpasero we should remove this logic in favor of the + // support in the super class so that we have 1 consistent + // solution for handling this. + + if (!this.isCorrelated(watcher.request)) { + this.legacyMonitorRequest(watcher); + } + } + + private legacyMonitorRequest(watcher: IParcelWatcherInstance): void { const parentPath = dirname(watcher.request.path); if (existsSync(parentPath)) { + this.trace('Trying to watch on the parent path to restart the watcher...', watcher); + const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, changes => { if (watcher.token.isCancellationRequested) { return; // return early when disposed @@ -475,19 +504,21 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Watcher path came back! Restart watching... for (const { resource, type } of changes) { - if (resource.fsPath === watcher.request.path && (type === FileChangeType.ADDED || type === FileChangeType.UPDATED)) { - this.warn('Watcher restarts because watched path got created again', watcher); + if (isEqual(resource.fsPath, watcher.request.path, !isLinux) && (type === FileChangeType.ADDED || type === FileChangeType.UPDATED)) { + if (this.isPathValid(watcher.request.path)) { + this.warn('Watcher restarts because watched path got created again', watcher); - // Stop watching that parent folder - nodeWatcher.dispose(); + // Stop watching that parent folder + nodeWatcher.dispose(); - // Restart the file watching - this.restartWatching(watcher); + // Restart the file watching + this.restartWatching(watcher); - break; + break; + } } } - }, msg => this._onDidLogMessage.fire(msg), this.verboseLogging); + }, undefined, msg => this._onDidLogMessage.fire(msg), this.verboseLogging); // Make sure to stop watching when the watcher is disposed watcher.token.onCancellationRequested(() => nodeWatcher.dispose()); @@ -520,12 +551,12 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } } - async stop(): Promise { - for (const [path] of this.watchers) { - await this.stopWatching(path); - } + override async stop(): Promise { + await super.stop(); - this.watchers.clear(); + for (const watcher of this.watchers) { + await this.stopWatching(watcher); + } } protected restartWatching(watcher: IParcelWatcherInstance, delay = 800): void { @@ -540,7 +571,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Await the watcher having stopped, as this is // needed to properly re-watch the same path - await this.stopWatching(watcher.request.path); + await this.stopWatching(watcher); // Start watcher again counting the restarts if (watcher.request.pollingInterval) { @@ -554,29 +585,26 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { watcher.token.onCancellationRequested(() => scheduler.dispose()); } - private async stopWatching(path: string): Promise { - const watcher = this.watchers.get(path); - if (watcher) { - this.trace(`stopping file watcher on ${watcher.request.path}`); + private async stopWatching(watcher: IParcelWatcherInstance): Promise { + this.trace(`stopping file watcher`, watcher); - this.watchers.delete(path); + this.watchers.delete(watcher); - try { - await watcher.stop(); - } catch (error) { - this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher); - } + try { + await watcher.stop(); + } catch (error) { + this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher); } } - protected normalizeRequests(requests: IRecursiveWatchRequest[], validatePaths = true): IRecursiveWatchRequest[] { + protected removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): IRecursiveWatchRequest[] { // Sort requests by path length to have shortest first // to have a way to prevent children to be watched if // parents exist. requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length); - // Map request paths to correlation and ignore identical paths + // Ignore requests for the same paths that have the same correlation const mapCorrelationtoRequests = new Map>(); for (const request of requests) { if (request.excludes.includes(GLOBSTAR)) { @@ -591,6 +619,10 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation); } + if (requestsForCorrelation.has(path)) { + this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`); + } + requestsForCorrelation.set(path, request); } @@ -616,31 +648,24 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { try { const realpath = realpathSync(request.path); if (realpath === request.path) { - this.trace(`ignoring a path for watching who's parent is already watched: ${request.path}`); + this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`); continue; } } catch (error) { - this.trace(`ignoring a path for watching who's realpath failed to resolve: ${request.path} (error: ${error})`); + this.trace(`ignoring a request for watching who's realpath failed to resolve: ${this.requestToString(request)} (error: ${error})`); + + this._onDidWatchFail.fire(request); continue; } } // Check for invalid paths - if (validatePaths) { - try { - const stat = statSync(request.path); - if (!stat.isDirectory()) { - this.trace(`ignoring a path for watching that is a file and not a folder: ${request.path}`); + if (validatePaths && !this.isPathValid(request.path)) { + this._onDidWatchFail.fire(request); - continue; - } - } catch (error) { - this.trace(`ignoring a path for watching who's stat info failed to resolve: ${request.path} (error: ${error})`); - - continue; - } + continue; } requestTrie.set(request.path, request); @@ -652,17 +677,34 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return normalizedRequests; } + private isPathValid(path: string): boolean { + try { + const stat = statSync(path); + if (!stat.isDirectory()) { + this.trace(`ignoring a path for watching that is a file and not a folder: ${path}`); + + return false; + } + } catch (error) { + this.trace(`ignoring a path for watching who's stat info failed to resolve: ${path} (error: ${error})`); + + return false; + } + + return true; + } + async setVerboseLogging(enabled: boolean): Promise { this.verboseLogging = enabled; } - private trace(message: string) { + protected trace(message: string, watcher?: IParcelWatcherInstance): void { if (this.verboseLogging) { - this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message) }); + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) }); } } - private warn(message: string, watcher?: IParcelWatcherInstance) { + protected warn(message: string, watcher?: IParcelWatcherInstance) { this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) }); } diff --git a/code/src/vs/platform/files/node/watcher/watcher.ts b/code/src/vs/platform/files/node/watcher/watcher.ts index e266239cb65..d0b563e540c 100644 --- a/code/src/vs/platform/files/node/watcher/watcher.ts +++ b/code/src/vs/platform/files/node/watcher/watcher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { INonRecursiveWatchRequest, IRecursiveWatchRequest, IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher'; +import { IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher'; import { Event } from 'vs/base/common/event'; import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; @@ -20,20 +20,9 @@ export class UniversalWatcher extends Disposable implements IUniversalWatcher { readonly onDidError = Event.any(this.recursiveWatcher.onDidError, this.nonRecursiveWatcher.onDidError); async watch(requests: IUniversalWatchRequest[]): Promise { - const recursiveWatchRequests: IRecursiveWatchRequest[] = []; - const nonRecursiveWatchRequests: INonRecursiveWatchRequest[] = []; - - for (const request of requests) { - if (request.recursive) { - recursiveWatchRequests.push(request); - } else { - nonRecursiveWatchRequests.push(request); - } - } - await Promises.settled([ - this.recursiveWatcher.watch(recursiveWatchRequests), - this.nonRecursiveWatcher.watch(nonRecursiveWatchRequests) + this.recursiveWatcher.watch(requests.filter(request => request.recursive)), + this.nonRecursiveWatcher.watch(requests.filter(request => !request.recursive)) ]); } diff --git a/code/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/code/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 74dbb343c97..177756f62eb 100644 --- a/code/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/code/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -9,17 +9,18 @@ import { Promises, RimRafMode } from 'vs/base/node/pfs'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileChangeType } from 'vs/platform/files/common/files'; import { INonRecursiveWatchRequest } from 'vs/platform/files/common/watcher'; -import { NodeJSFileWatcherLibrary, watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; +import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { getDriveLetter } from 'vs/base/common/extpath'; import { ltrim } from 'vs/base/common/strings'; -import { DeferredPromise } from 'vs/base/common/async'; +import { DeferredPromise, timeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; import { FileAccess } from 'vs/base/common/network'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; +import { Emitter, Event } from 'vs/base/common/event'; // this suite has shown flaky runs in Azure pipelines where // tasks would just hang and timeout after a while (not in @@ -30,27 +31,20 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; class TestNodeJSWatcher extends NodeJSWatcher { - override async watch(requests: INonRecursiveWatchRequest[]): Promise { - await super.watch(requests); - await this.whenReady(); - } - - async whenReady(): Promise { - for (const [, watcher] of this.watchers) { - await watcher.instance.ready; - } - } - } + protected override readonly suspendedWatchRequestPollingInterval = 100; - class TestNodeJSFileWatcherLibrary extends NodeJSFileWatcherLibrary { + private readonly _onDidWatch = this._register(new Emitter()); + readonly onDidWatch = this._onDidWatch.event; - private readonly _whenDisposed = new DeferredPromise(); - readonly whenDisposed = this._whenDisposed.p; + readonly onWatchFail = this._onDidWatchFail.event; - override dispose(): void { - super.dispose(); + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { + await super.doWatch(requests); + for (const watcher of this.watchers) { + await watcher.instance.ready; + } - this._whenDisposed.complete(); + this._onDidWatch.fire(); } } @@ -432,7 +426,7 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; return basicCrudTest(join(link, 'newFile.txt')); }); - async function basicCrudTest(filePath: string, skipAdd?: boolean, correlationId?: number | null, expectedCount?: number): Promise { + async function basicCrudTest(filePath: string, skipAdd?: boolean, correlationId?: number | null, expectedCount?: number, awaitWatchAfterAdd?: boolean): Promise { let changeFuture: Promise; // New file @@ -440,6 +434,9 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, correlationId, expectedCount); await Promises.writeFile(filePath, 'Hello World'); await changeFuture; + if (awaitWatchAfterAdd) { + await Event.toPromise(watcher.onDidWatch); + } } // Change file @@ -506,27 +503,6 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; await watcher.watch([{ path: invalidPath, excludes: [], recursive: false }]); }); - (isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('deleting watched path is handled properly (folder watch)', async function () { - const watchedPath = join(testDir, 'deep'); - - const watcher = new TestNodeJSFileWatcherLibrary({ path: watchedPath, excludes: [], recursive: false }, changes => { }); - await watcher.ready; - - // Delete watched path and ensure watcher is now disposed - Promises.rm(watchedPath, RimRafMode.UNLINK); - await watcher.whenDisposed; - }); - - test('deleting watched path is handled properly (file watch)', async function () { - const watchedPath = join(testDir, 'lorem.txt'); - const watcher = new TestNodeJSFileWatcherLibrary({ path: watchedPath, excludes: [], recursive: false }, changes => { }); - await watcher.ready; - - // Delete watched path and ensure watcher is now disposed - Promises.unlink(watchedPath); - await watcher.whenDisposed; - }); - test('watchFileContents', async function () { const watchedPath = join(testDir, 'lorem.txt'); @@ -547,16 +523,130 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; return watchPromise; }); - test('watching same or overlapping paths supported when correlation is applied', async () => { + test('watching same or overlapping paths supported when correlation is applied', async function () { + await watcher.watch([ + { path: testDir, excludes: [], recursive: false, correlationId: 1 } + ]); + + await basicCrudTest(join(testDir, 'newFile_1.txt'), undefined, null, 1); - // same path, same options await watcher.watch([ { path: testDir, excludes: [], recursive: false, correlationId: 1 }, { path: testDir, excludes: [], recursive: false, correlationId: 2, }, { path: testDir, excludes: [], recursive: false, correlationId: undefined } ]); - await basicCrudTest(join(testDir, 'newFile.txt'), undefined, null, 3); + await basicCrudTest(join(testDir, 'newFile_2.txt'), undefined, null, 3); await basicCrudTest(join(testDir, 'otherNewFile.txt'), undefined, null, 3); }); + + test('watching missing path emits watcher fail event', async function () { + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + + const folderPath = join(testDir, 'missing'); + watcher.watch([{ path: folderPath, excludes: [], recursive: true }]); + + await onDidWatchFail; + }); + + test('deleting watched path emits watcher fail and delete event when correlated (file watch)', async function () { + const filePath = join(testDir, 'lorem.txt'); + + await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); + Promises.unlink(filePath); + await onDidWatchFail; + await changeFuture; + }); + + (isMacintosh /* macOS: does not seem to report deletes on folders */ ? test.skip : test)('deleting watched path emits watcher fail and delete event when correlated (folder watch)', async function () { + const folderPath = join(testDir, 'deep'); + + await watcher.watch([{ path: folderPath, excludes: [], recursive: false }]); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1); + Promises.rm(folderPath, RimRafMode.UNLINK); + await onDidWatchFail; + await changeFuture; + }); + + test('correlated watch requests support suspend/resume (file, does not exist in beginning)', async function () { + const filePath = join(testDir, 'not-found.txt'); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + await onDidWatchFail; + + await basicCrudTest(filePath, undefined, 1, undefined, true); + await basicCrudTest(filePath, undefined, 1, undefined, true); + }); + + test('correlated watch requests support suspend/resume (file, exists in beginning)', async function () { + const filePath = join(testDir, 'lorem.txt'); + await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await basicCrudTest(filePath, true, 1); + await onDidWatchFail; + + await basicCrudTest(filePath, undefined, 1, undefined, true); + }); + + test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async function () { + let onDidWatchFail = Event.toPromise(watcher.onWatchFail); + + const folderPath = join(testDir, 'not-found'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); + await onDidWatchFail; + + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + let onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, undefined, 1); + + if (!isMacintosh) { // macOS does not report DELETE events for folders + onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rmdir(folderPath); + await onDidWatchFail; + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + await timeout(500); // somehow needed on Linux + + await basicCrudTest(filePath, undefined, 1); + } + }); + + (isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exists in beginning)', async function () { + const folderPath = join(testDir, 'deep'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); + + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, undefined, 1); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; + + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + const onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + await timeout(500); // somehow needed on Linux + + await basicCrudTest(filePath, undefined, 1); + }); }); diff --git a/code/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/code/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 42e8b4ad730..18a1a538433 100644 --- a/code/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/code/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -20,6 +20,7 @@ import { FileAccess } from 'vs/base/common/network'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; +import { Emitter, Event } from 'vs/base/common/event'; // this suite has shown flaky runs in Azure pipelines where // tasks would just hang and timeout after a while (not in @@ -30,23 +31,32 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; class TestParcelWatcher extends ParcelWatcher { - testNormalizePaths(paths: string[], excludes: string[] = []): string[] { + protected override readonly suspendedWatchRequestPollingInterval = 100; + + private readonly _onDidWatch = this._register(new Emitter()); + readonly onDidWatch = this._onDidWatch.event; + + readonly onWatchFail = this._onDidWatchFail.event; + + testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): string[] { // Work with strings as paths to simplify testing const requests: IRecursiveWatchRequest[] = paths.map(path => { return { path, excludes, recursive: true }; }); - return this.normalizeRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); + return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); } - override async watch(requests: IRecursiveWatchRequest[]): Promise { - await super.watch(requests); + protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { + await super.doWatch(requests); await this.whenReady(); + + this._onDidWatch.fire(); } async whenReady(): Promise { - for (const [, watcher] of this.watchers) { + for (const watcher of this.watchers) { await watcher.ready; } } @@ -542,7 +552,7 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; await watcher.watch([{ path: invalidPath, excludes: [], recursive: true }]); }); - (isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path is handled properly', async function () { + (isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path without correlation restarts watching', async function () { const watchedPath = join(testDir, 'deep'); await watcher.watch([{ path: watchedPath, excludes: [], recursive: true }]); @@ -574,35 +584,40 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; test('should not exclude roots that do not overlap', () => { if (isWindows) { - assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a']), ['C:\\a']); - assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a']), ['C:\\a']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); } else { - assert.deepStrictEqual(watcher.testNormalizePaths(['/a']), ['/a']); - assert.deepStrictEqual(watcher.testNormalizePaths(['/a', '/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testNormalizePaths(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a']), ['/a']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); } }); test('should remove sub-folders of other paths', () => { if (isWindows) { - assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a', 'C:\\a\\b']), ['C:\\a']); - assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b']), ['C:\\a']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); } else { - assert.deepStrictEqual(watcher.testNormalizePaths(['/a', '/a/b']), ['/a']); - assert.deepStrictEqual(watcher.testNormalizePaths(['/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testNormalizePaths(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testNormalizePaths(['/a', '/a/b', '/a/c/d']), ['/a']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/a/b']), ['/a']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/a/b', '/a/c/d']), ['/a']); } }); test('should ignore when everything excluded', () => { - assert.deepStrictEqual(watcher.testNormalizePaths(['/foo/bar', '/bar'], ['**', 'something']), []); + assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/foo/bar', '/bar'], ['**', 'something']), []); }); test('watching same or overlapping paths supported when correlation is applied', async () => { + await watcher.watch([ + { path: testDir, excludes: [], recursive: true, correlationId: 1 } + ]); + + await basicCrudTest(join(testDir, 'newFile.txt'), null, 1); // same path, same options await watcher.watch([ @@ -646,4 +661,74 @@ import { addUNCHostToAllowlist } from 'vs/base/node/unc'; await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3); await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3); }); + + test('watching missing path emits watcher fail event', async function () { + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + + const folderPath = join(testDir, 'missing'); + watcher.watch([{ path: folderPath, excludes: [], recursive: true }]); + + await onDidWatchFail; + }); + + test('deleting watched path emits watcher fail and delete event if correlated', async function () { + const folderPath = join(testDir, 'deep'); + + await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1); + Promises.rm(folderPath, RimRafMode.UNLINK); + await onDidWatchFail; + await changeFuture; + }); + + test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async () => { + let onDidWatchFail = Event.toPromise(watcher.onWatchFail); + + const folderPath = join(testDir, 'not-found'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + await onDidWatchFail; + + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + let onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, 1); + + onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; + + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + await basicCrudTest(filePath, 1); + }); + + test('correlated watch requests support suspend/resume (folder, exist in beginning)', async () => { + const folderPath = join(testDir, 'deep'); + await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath, 1); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; + + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + const onDidWatch = Event.toPromise(watcher.onDidWatch); + await Promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; + + await basicCrudTest(filePath, 1); + }); }); diff --git a/code/src/vs/platform/hover/browser/hover.ts b/code/src/vs/platform/hover/browser/hover.ts index 82d9574ca06..9213e1ea02f 100644 --- a/code/src/vs/platform/hover/browser/hover.ts +++ b/code/src/vs/platform/hover/browser/hover.ts @@ -7,10 +7,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IHoverDelegate, IHoverDelegateOptions, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { addStandardDisposableListener } from 'vs/base/browser/dom'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; export const IHoverService = createDecorator('hoverService'); @@ -235,12 +236,12 @@ export interface IHoverTarget extends IDisposable { export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate { - private lastHoverHideTime = Number.MAX_VALUE; + private lastHoverHideTime = 0; private timeLimit = 200; private _delay: number; get delay(): number { - if (this.instantHover && Date.now() - this.lastHoverHideTime < this.timeLimit) { + if (this.isInstantlyHovering()) { return 0; // show instantly when a hover was recently shown } return this._delay; @@ -279,16 +280,29 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate })); } + const id = options.content instanceof HTMLElement ? undefined : options.content.toString(); + return this.hoverService.showHover({ ...options, + ...overrideOptions, persistence: { - hideOnHover: true, hideOnKeyDown: true, + ...overrideOptions.persistence }, - ...overrideOptions + id, + appearance: { + ...options.appearance, + compact: true, + skipFadeInAnimation: this.isInstantlyHovering(), + ...overrideOptions.appearance + } }, focus); } + private isInstantlyHovering(): boolean { + return this.instantHover && Date.now() - this.lastHoverHideTime < this.timeLimit; + } + setInstantHoverTimeLimit(timeLimit: number): void { if (!this.instantHover) { throw new Error('Instant hover is not enabled'); diff --git a/code/src/vs/platform/issue/common/issue.ts b/code/src/vs/platform/issue/common/issue.ts index d1c4e29bb63..df2a1e27599 100644 --- a/code/src/vs/platform/issue/common/issue.ts +++ b/code/src/vs/platform/issue/common/issue.ts @@ -25,6 +25,12 @@ export const enum IssueType { FeatureRequest } +export enum IssueSource { + VSCode = 'vscode', + Extension = 'extension', + Marketplace = 'marketplace' +} + export interface IssueReporterStyles extends WindowStyles { textLinkColor?: string; textLinkActiveForeground?: string; @@ -65,6 +71,7 @@ export interface IssueReporterData extends WindowData { styles: IssueReporterStyles; enabledExtensions: IssueReporterExtensionData[]; issueType?: IssueType; + issueSource?: IssueSource; extensionId?: string; experiments?: string; restrictedMode: boolean; diff --git a/code/src/vs/platform/layout/browser/layoutService.ts b/code/src/vs/platform/layout/browser/layoutService.ts index 6fad206c73e..65de0312451 100644 --- a/code/src/vs/platform/layout/browser/layoutService.ts +++ b/code/src/vs/platform/layout/browser/layoutService.ts @@ -84,6 +84,15 @@ export interface ILayoutService { */ getContainer(window: Window): HTMLElement; + /** + * Ensures that the styles for the container associated + * to the window have loaded. For the main window, this + * will resolve instantly, but for floating windows, this + * will resolve once the styles have been loaded and helps + * for when certain layout assumptions are made. + */ + whenContainerStylesLoaded(window: Window): Promise | undefined; + /** * An offset to use for positioning elements inside the main container. */ @@ -94,13 +103,6 @@ export interface ILayoutService { */ readonly activeContainerOffset: ILayoutOffsetInfo; - /** - * A promise resolved when the stylesheets for the active container have been - * loaded. Aux windows load their styles asynchronously, so there may be - * an initial delay before resolution happens. - */ - readonly whenActiveContainerStylesLoaded: Promise; - /** * Focus the primary component of the active container. */ diff --git a/code/src/vs/platform/log/common/log.ts b/code/src/vs/platform/log/common/log.ts index e2d69c0fe30..28fc419bbaf 100644 --- a/code/src/vs/platform/log/common/log.ts +++ b/code/src/vs/platform/log/common/log.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; @@ -12,6 +13,7 @@ import { isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import { Mutable, isNumber, isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { ILocalizedString } from 'vs/platform/action/common/action'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -745,6 +747,17 @@ export function LogLevelToString(logLevel: LogLevel): string { } } +export function LogLevelToLocalizedString(logLevel: LogLevel): ILocalizedString { + switch (logLevel) { + case LogLevel.Trace: return { original: 'Trace', value: nls.localize('trace', "Trace") }; + case LogLevel.Debug: return { original: 'Debug', value: nls.localize('debug', "Debug") }; + case LogLevel.Info: return { original: 'Info', value: nls.localize('info', "Info") }; + case LogLevel.Warning: return { original: 'Warning', value: nls.localize('warn', "Warning") }; + case LogLevel.Error: return { original: 'Error', value: nls.localize('error', "Error") }; + case LogLevel.Off: return { original: 'Off', value: nls.localize('off', "Off") }; + } +} + export function parseLogLevel(logLevel: string): LogLevel | undefined { switch (logLevel) { case 'trace': diff --git a/code/src/vs/platform/markers/common/markerService.ts b/code/src/vs/platform/markers/common/markerService.ts index 0f2ef2566fc..5294be4aefa 100644 --- a/code/src/vs/platform/markers/common/markerService.ts +++ b/code/src/vs/platform/markers/common/markerService.ts @@ -12,7 +12,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IMarker, IMarkerData, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers'; -export const unsupportedSchemas = new Set([Schemas.inMemory, Schemas.vscodeSourceControl, Schemas.walkThrough, Schemas.walkThroughSnippet]); +export const unsupportedSchemas = new Set([Schemas.inMemory, Schemas.vscodeSourceControl, Schemas.walkThrough, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock]); class DoubleResourceMap { diff --git a/code/src/vs/platform/menubar/electron-main/menubar.ts b/code/src/vs/platform/menubar/electron-main/menubar.ts index 2ab5bcecbfa..f11b38cb19d 100644 --- a/code/src/vs/platform/menubar/electron-main/menubar.ts +++ b/code/src/vs/platform/menubar/electron-main/menubar.ts @@ -647,7 +647,7 @@ export class Menubar { return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; case StateType.Downloaded: - return [new MenuItem({ + return isMacintosh ? [] : [new MenuItem({ label: this.mnemonicLabel(nls.localize('miInstallUpdate', "Install &&Update...")), click: () => { this.reportMenuActionTelemetry('InstallUpdate'); this.updateService.applyUpdate(); diff --git a/code/src/vs/platform/opener/browser/link.ts b/code/src/vs/platform/opener/browser/link.ts index 2b455fa8dc6..a52cad5c5f0 100644 --- a/code/src/vs/platform/opener/browser/link.ts +++ b/code/src/vs/platform/opener/browser/link.ts @@ -12,6 +12,8 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import 'vs/css!./link'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface ILinkDescriptor { readonly label: string | HTMLElement; @@ -28,6 +30,8 @@ export interface ILinkOptions { export class Link extends Disposable { private el: HTMLAnchorElement; + private hover?: ICustomHover; + private _enabled: boolean = true; get enabled(): boolean { @@ -68,9 +72,7 @@ export class Link extends Disposable { this.el.tabIndex = link.tabIndex; } - if (typeof link.title !== 'undefined') { - this.el.title = link.title; - } + this.setTooltip(link.title); this._link = link; } @@ -86,9 +88,10 @@ export class Link extends Disposable { this.el = append(container, $('a.monaco-link', { tabIndex: _link.tabIndex ?? 0, href: _link.href, - title: _link.title }, _link.label)); + this.setTooltip(_link.title); + this.el.setAttribute('role', 'button'); const onClickEmitter = this._register(new DomEmitter(this.el, 'click')); @@ -117,4 +120,12 @@ export class Link extends Disposable { this.enabled = true; } + + private setTooltip(title: string | undefined): void { + if (!this.hover && title) { + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.el, title)); + } else if (this.hover) { + this.hover.update(title); + } + } } diff --git a/code/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/code/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 87a211bcfbf..7aabb2705dd 100644 --- a/code/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/code/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -19,6 +19,7 @@ import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/co import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILogService } from 'vs/platform/log/common/log'; import { FastAndSlowPicks, IPickerQuickAccessItem, IPickerQuickAccessProviderOptions, PickerQuickAccessProvider, Picks } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; @@ -332,6 +333,7 @@ export class CommandsHistory extends Disposable { constructor( @IStorageService private readonly storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService ) { super(); @@ -373,7 +375,7 @@ export class CommandsHistory extends Disposable { try { serializedCache = JSON.parse(raw); } catch (error) { - // invalid data + this.logService.error(`[CommandsHistory] invalid data: ${error}`); } } diff --git a/code/src/vs/platform/quickinput/browser/media/quickInput.css b/code/src/vs/platform/quickinput/browser/media/quickInput.css index 8756dc7c60a..f8e7aba2b91 100644 --- a/code/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/code/src/vs/platform/quickinput/browser/media/quickInput.css @@ -16,8 +16,7 @@ .quick-input-titlebar { display: flex; align-items: center; - border-top-left-radius: 5px; /* match border radius of quick input widget */ - border-top-right-radius: 5px; + border-radius: inherit; } .quick-input-left-action-bar { @@ -266,6 +265,9 @@ .quick-input-list .monaco-highlighted-label .highlight { font-weight: bold; + /* preserve list-like styling instead of tree-like styling */ + color: var(--vscode-list-focusHighlightForeground) !important; + background-color: transparent; } .quick-input-list .quick-input-list-entry .quick-input-list-separator { @@ -301,7 +303,9 @@ .quick-input-list .quick-input-list-entry .quick-input-list-entry-action-bar .action-label.always-visible, .quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar .action-label, -.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label { +.quick-input-list .quick-input-list-entry.focus-inside .quick-input-list-entry-action-bar .action-label, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label, +.quick-input-list .monaco-list-row.passive-focused .quick-input-list-entry-action-bar .action-label { display: flex; } @@ -319,3 +323,18 @@ font-weight: 600; font-size: 12px; } + +/* Hide border when the item becomes the sticky one */ +.quick-input-list .monaco-tree-sticky-row .quick-input-list-entry.quick-input-list-separator-as-item.quick-input-list-separator-border { + border-top-style: none; +} + +/* Give sticky row the same padding as the scrollable list */ +.quick-input-list .monaco-tree-sticky-row { + padding: 0 5px; +} + +/* Hide the twistie containers so that there isn't blank indent */ +.quick-input-list .monaco-tl-twistie { + display: none !important; +} diff --git a/code/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/code/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index acde1e461d1..86160c9e26a 100644 --- a/code/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/code/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -6,7 +6,7 @@ import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { isFunction } from 'vs/base/common/types'; @@ -59,6 +59,22 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; } +export interface IPickerQuickAccessSeparator extends IQuickPickSeparator { + /** + * A method that will be executed when a button of the pick item was + * clicked on. + * + * @param buttonIndex index of the button of the item that + * was clicked. + * + * @param the state of modifier keys when the button was triggered. + * + * @returns a value that indicates what should happen after the trigger + * which can be a `Promise` for long running operations. + */ + trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; +} + export interface IPickerQuickAccessProviderOptions { /** @@ -320,47 +336,52 @@ export abstract class PickerQuickAccessProvider { - if (typeof item.trigger === 'function') { - const buttonIndex = item.buttons?.indexOf(button) ?? -1; - if (buttonIndex >= 0) { - const result = item.trigger(buttonIndex, picker.keyMods); - const action = (typeof result === 'number') ? result : await result; - - if (token.isCancellationRequested) { - return; - } + const buttonTrigger = async (button: IQuickInputButton, item: T | IPickerQuickAccessSeparator) => { + if (typeof item.trigger !== 'function') { + return; + } - switch (action) { - case TriggerAction.NO_ACTION: - break; - case TriggerAction.CLOSE_PICKER: - picker.hide(); - break; - case TriggerAction.REFRESH_PICKER: - updatePickerItems(); - break; - case TriggerAction.REMOVE_ITEM: { - const index = picker.items.indexOf(item); - if (index !== -1) { - const items = picker.items.slice(); - const removed = items.splice(index, 1); - const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); - const keepScrollPositionBefore = picker.keepScrollPosition; - picker.keepScrollPosition = true; - picker.items = items; - if (activeItems) { - picker.activeItems = activeItems; - } - picker.keepScrollPosition = keepScrollPositionBefore; + const buttonIndex = item.buttons?.indexOf(button) ?? -1; + if (buttonIndex >= 0) { + const result = item.trigger(buttonIndex, picker.keyMods); + const action = (typeof result === 'number') ? result : await result; + + if (token.isCancellationRequested) { + return; + } + + switch (action) { + case TriggerAction.NO_ACTION: + break; + case TriggerAction.CLOSE_PICKER: + picker.hide(); + break; + case TriggerAction.REFRESH_PICKER: + updatePickerItems(); + break; + case TriggerAction.REMOVE_ITEM: { + const index = picker.items.indexOf(item); + if (index !== -1) { + const items = picker.items.slice(); + const removed = items.splice(index, 1); + const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); + const keepScrollPositionBefore = picker.keepScrollPosition; + picker.keepScrollPosition = true; + picker.items = items; + if (activeItems) { + picker.activeItems = activeItems; } - break; + picker.keepScrollPosition = keepScrollPositionBefore; } + break; } } } - })); + }; + + // Trigger the pick with button index if button triggered + disposables.add(picker.onDidTriggerItemButton(({ button, item }) => buttonTrigger(button, item))); + disposables.add(picker.onDidTriggerSeparatorButton(({ button, separator }) => buttonTrigger(button, separator))); return disposables; } diff --git a/code/src/vs/platform/quickinput/browser/quickInput.ts b/code/src/vs/platform/quickinput/browser/quickInput.ts index 96dfeee7ea8..3618afe711b 100644 --- a/code/src/vs/platform/quickinput/browser/quickInput.ts +++ b/code/src/vs/platform/quickinput/browser/quickInput.ts @@ -8,11 +8,10 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { CountBadge, ICountBadgeStyles } from 'vs/base/browser/ui/countBadge/countBadge'; -import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; +import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { IProgressBarStyles, ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { equals } from 'vs/base/common/arrays'; @@ -21,17 +20,17 @@ 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 } from 'vs/base/common/lifecycle'; -import { isIOS } from 'vs/base/common/platform'; +import { isIOS, isMacintosh } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./media/quickInput'; import { localize } from 'vs/nls'; 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 { quickInputButtonToAction, renderQuickInputDescription } from './quickInputUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHoverOptions, IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { QuickInputListFocus, QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; export interface IQuickInputOptions { idPrefix: string; @@ -41,13 +40,6 @@ export interface IQuickInputOptions { setContextKey(id?: string): void; linkOpenerDelegate(content: string): void; returnFocus(): void; - createList( - user: string, - container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], - options: IListOptions, - ): List; /** * @todo With IHover in vs/editor, can we depend on the service directly * instead of passing it through a hover delegate? @@ -108,7 +100,7 @@ export interface QuickInputUI { customButtonContainer: HTMLElement; customButton: Button; progressBar: ProgressBar; - list: QuickInputList; + list: QuickInputTree; onDidAccept: Event; onDidCustom: Event; onDidTriggerButton: Event; @@ -162,6 +154,7 @@ class QuickInput extends Disposable implements IQuickInput { private _lastSeverity: Severity | undefined; private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); private readonly onDidHideEmitter = this._register(new Emitter()); + private readonly onWillHideEmitter = this._register(new Emitter()); private readonly onDisposeEmitter = this._register(new Emitter()); protected readonly visibleDisposables = this._register(new DisposableStore()); @@ -352,6 +345,11 @@ class QuickInput extends Disposable implements IQuickInput { readonly onDidHide = this.onDidHideEmitter.event; + willHide(reason = QuickInputHideReason.Other): void { + this.onWillHideEmitter.fire({ reason }); + } + readonly onWillHide = this.onWillHideEmitter.event; + protected update() { if (!this.visible) { return; @@ -820,20 +818,25 @@ export class QuickPick extends QuickInput implements I this.ui.inputBox.onDidChange(value => { this.doSetValue(value, true /* skip update since this originates from the UI */); })); + // Keybindings for the input box or list if there is no input box this.visibleDisposables.add((this._hideInput ? this.ui.list : this.ui.inputBox).onKeyDown((event: KeyboardEvent | StandardKeyboardEvent) => { switch (event.keyCode) { case KeyCode.DownArrow: - this.ui.list.focus(QuickInputListFocus.Next); + if (isMacintosh ? event.metaKey : event.altKey) { + this.ui.list.focus(QuickInputListFocus.NextSeparator); + } else { + this.ui.list.focus(QuickInputListFocus.Next); + } if (this.canSelectMany) { this.ui.list.domFocus(); } dom.EventHelper.stop(event, true); break; case KeyCode.UpArrow: - if (this.ui.list.getFocusedElements().length) { - this.ui.list.focus(QuickInputListFocus.Previous); + if (isMacintosh ? event.metaKey : event.altKey) { + this.ui.list.focus(QuickInputListFocus.PreviousSeparator); } else { - this.ui.list.focus(QuickInputListFocus.Last); + this.ui.list.focus(QuickInputListFocus.Previous); } if (this.canSelectMany) { this.ui.list.domFocus(); @@ -1066,6 +1069,7 @@ export class QuickPick extends QuickInput implements I this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; + const currentActiveItems = this._activeItems; this.ui.list.setElements(this.items); this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); @@ -1073,6 +1077,15 @@ export class QuickPick extends QuickInput implements I this.ui.count.setCount(this.ui.list.getCheckedCount()); switch (this._itemActivation) { case ItemActivation.NONE: + // Handle the case where we had active items (i.e. someone chose an item) + // but the initial item activation is set to none. Calling clearFocus will + // not trigger the onDidFocus event because when the tree receives new elements, + // it sets the focus to no elements. So we need to set & fire the active items + // accordingly to reflect the state change after setting the items. + if (currentActiveItems.length > 0) { + this._activeItems = []; + this.onDidChangeActiveEmitter.fire(this._activeItems); + } this._itemActivation = ItemActivation.FIRST; // only valid once, then unset break; case ItemActivation.SECOND: diff --git a/code/src/vs/platform/quickinput/browser/quickInputController.ts b/code/src/vs/platform/quickinput/browser/quickInputController.ts index d5a23ae66e8..e64440ba2a7 100644 --- a/code/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/code/src/vs/platform/quickinput/browser/quickInputController.ts @@ -18,11 +18,11 @@ 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'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { mainWindow } from 'vs/base/browser/window'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { QuickInputListFocus, QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; const $ = dom.$; @@ -54,9 +54,10 @@ export class QuickInputController extends Disposable { private previousFocusElement?: HTMLElement; - constructor(private options: IQuickInputOptions, - private readonly themeService: IThemeService, - private readonly layoutService: ILayoutService + constructor( + private options: IQuickInputOptions, + @ILayoutService private readonly layoutService: ILayoutService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.idPrefix = options.idPrefix; @@ -172,7 +173,7 @@ export class QuickInputController extends Disposable { const description1 = dom.append(container, $('.quick-input-description')); const listId = this.idPrefix + 'list'; - const list = this._register(new QuickInputList(container, listId, this.options, this.themeService)); + const list = this._register(this.instantiationService.createInstance(QuickInputTree, container, this.options.hoverDelegate, this.options.linkOpenerDelegate, listId)); inputBox.setAttribute('aria-controls', listId); this._register(list.onDidChangeFocus(() => { inputBox.setAttribute('aria-activedescendant', list.getActiveDescendant() ?? ''); @@ -219,6 +220,7 @@ export class QuickInputController extends Disposable { inputBox.setFocus(); })); // TODO: Turn into commands instead of handling KEY_DOWN + // Keybindings for the quickinput widget as a whole 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. @@ -614,6 +616,7 @@ export class QuickInputController extends Disposable { if (!controller) { return; } + controller.willHide(reason); const container = this.ui?.container; const focusChanged = container && !dom.isAncestorOfActiveElement(container); diff --git a/code/src/vs/platform/quickinput/browser/quickInputList.ts b/code/src/vs/platform/quickinput/browser/quickInputList.ts deleted file mode 100644 index de219a800bb..00000000000 --- a/code/src/vs/platform/quickinput/browser/quickInputList.ts +++ /dev/null @@ -1,1145 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { AriaRole } from 'vs/base/browser/ui/aria/aria'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IHoverDelegate, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListAccessibilityProvider, IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; -import { range } from 'vs/base/common/arrays'; -import { ThrottledDelayer } from 'vs/base/common/async'; -import { compareAnything } from 'vs/base/common/comparers'; -import { memoize } from 'vs/base/common/decorators'; -import { isCancellationError } from 'vs/base/common/errors'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IMatch } from 'vs/base/common/filters'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { getCodiconAriaLabel, IParsedLabelWithIcons, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import * as platform from 'vs/base/common/platform'; -import { ltrim } from 'vs/base/common/strings'; -import 'vs/css!./media/quickInput'; -import { localize } from 'vs/nls'; -import { IQuickInputOptions } from 'vs/platform/quickinput/browser/quickInput'; -import { quickInputButtonToAction } from 'vs/platform/quickinput/browser/quickInputUtils'; -import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { Lazy } from 'vs/base/common/lazy'; -import { URI } from 'vs/base/common/uri'; -import { isDark } from 'vs/platform/theme/common/theme'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ITooltipMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; - -const $ = dom.$; - -interface IListElementLazyParts { - readonly saneLabel: string; - readonly saneSortLabel: string; - readonly saneAriaLabel: string; -} - -interface IListElement extends IListElementLazyParts { - readonly hasCheckbox: boolean; - readonly index: number; - readonly item?: IQuickPickItem; - readonly saneDescription?: string; - readonly saneDetail?: string; - readonly saneTooltip?: string | IMarkdownString | HTMLElement; - readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void; - readonly fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void; - readonly onChecked: Event; - checked: boolean; - hidden: boolean; - element?: HTMLElement; - labelHighlights?: IMatch[]; - descriptionHighlights?: IMatch[]; - detailHighlights?: IMatch[]; - separator?: IQuickPickSeparator; -} - -class ListElement implements IListElement { - private readonly _init: Lazy; - - readonly hasCheckbox: boolean; - readonly index: number; - readonly item?: IQuickPickItem; - readonly saneDescription?: string; - readonly saneDetail?: string; - readonly saneTooltip?: string | IMarkdownString | HTMLElement; - readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void; - readonly fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void; - - // state will get updated later - private _checked: boolean = false; - private _hidden: boolean = false; - private _element?: HTMLElement; - private _labelHighlights?: IMatch[]; - private _descriptionHighlights?: IMatch[]; - private _detailHighlights?: IMatch[]; - private _separator?: IQuickPickSeparator; - - private readonly _onChecked: Emitter<{ listElement: IListElement; checked: boolean }>; - onChecked: Event; - - constructor( - mainItem: QuickPickItem, - previous: QuickPickItem | undefined, - index: number, - hasCheckbox: boolean, - fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void, - fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void, - onCheckedEmitter: Emitter<{ listElement: IListElement; checked: boolean }> - ) { - this.hasCheckbox = hasCheckbox; - this.index = index; - this.fireButtonTriggered = fireButtonTriggered; - this.fireSeparatorButtonTriggered = fireSeparatorButtonTriggered; - this._onChecked = onCheckedEmitter; - this.onChecked = hasCheckbox - ? Event.map(Event.filter<{ listElement: IListElement; checked: boolean }>(this._onChecked.event, e => e.listElement === this), e => e.checked) - : Event.None; - - if (mainItem.type === 'separator') { - this._separator = mainItem; - } else { - this.item = mainItem; - if (previous && previous.type === 'separator' && !previous.buttons) { - this._separator = previous; - } - this.saneDescription = this.item.description; - this.saneDetail = this.item.detail; - this._labelHighlights = this.item.highlights?.label; - this._descriptionHighlights = this.item.highlights?.description; - this._detailHighlights = this.item.highlights?.detail; - this.saneTooltip = this.item.tooltip; - } - this._init = new Lazy(() => { - const saneLabel = mainItem.label ?? ''; - const saneSortLabel = parseLabelWithIcons(saneLabel).text.trim(); - - const saneAriaLabel = mainItem.ariaLabel || [saneLabel, this.saneDescription, this.saneDetail] - .map(s => getCodiconAriaLabel(s)) - .filter(s => !!s) - .join(', '); - - return { - saneLabel, - saneSortLabel, - saneAriaLabel - }; - }); - } - - // #region Lazy Getters - - get saneLabel() { - return this._init.value.saneLabel; - } - - get saneSortLabel() { - return this._init.value.saneSortLabel; - } - - get saneAriaLabel() { - return this._init.value.saneAriaLabel; - } - - // #endregion - - // #region Getters and Setters - - get element() { - return this._element; - } - - set element(value: HTMLElement | undefined) { - this._element = value; - } - - get hidden() { - return this._hidden; - } - - set hidden(value: boolean) { - this._hidden = value; - } - - get checked() { - return this._checked; - } - - set checked(value: boolean) { - if (value !== this._checked) { - this._checked = value; - this._onChecked.fire({ listElement: this, checked: value }); - } - } - - get separator() { - return this._separator; - } - - set separator(value: IQuickPickSeparator | undefined) { - this._separator = value; - } - - get labelHighlights() { - return this._labelHighlights; - } - - set labelHighlights(value: IMatch[] | undefined) { - this._labelHighlights = value; - } - - get descriptionHighlights() { - return this._descriptionHighlights; - } - - set descriptionHighlights(value: IMatch[] | undefined) { - this._descriptionHighlights = value; - } - - get detailHighlights() { - return this._detailHighlights; - } - - set detailHighlights(value: IMatch[] | undefined) { - this._detailHighlights = value; - } - - // #endregion -} - -interface IListElementTemplateData { - entry: HTMLDivElement; - checkbox: HTMLInputElement; - icon: HTMLDivElement; - label: IconLabel; - keybinding: KeybindingLabel; - detail: IconLabel; - separator: HTMLDivElement; - actionBar: ActionBar; - element: IListElement; - toDisposeElement: IDisposable[]; - toDisposeTemplate: IDisposable[]; -} - -class ListElementRenderer implements IListRenderer { - - static readonly ID = 'listelement'; - - constructor( - private readonly themeService: IThemeService, - private readonly hoverDelegate: IHoverDelegate | undefined, - ) { } - - get templateId() { - return ListElementRenderer.ID; - } - - renderTemplate(container: HTMLElement): IListElementTemplateData { - const data: IListElementTemplateData = Object.create(null); - data.toDisposeElement = []; - data.toDisposeTemplate = []; - - data.entry = dom.append(container, $('.quick-input-list-entry')); - - // Checkbox - const label = dom.append(data.entry, $('label.quick-input-list-label')); - data.toDisposeTemplate.push(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => { - if (!data.checkbox.offsetParent) { // If checkbox not visible: - e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740 - } - })); - data.checkbox = dom.append(label, $('input.quick-input-list-checkbox')); - data.checkbox.type = 'checkbox'; - data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { - data.element.checked = data.checkbox.checked; - })); - - // Rows - const rows = dom.append(label, $('.quick-input-list-rows')); - const row1 = dom.append(rows, $('.quick-input-list-row')); - const row2 = dom.append(rows, $('.quick-input-list-row')); - - // Label - data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportIcons: true, hoverDelegate: this.hoverDelegate }); - data.toDisposeTemplate.push(data.label); - data.icon = dom.prepend(data.label.element, $('.quick-input-list-icon')); - - // Keybinding - const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding')); - data.keybinding = new KeybindingLabel(keybindingContainer, platform.OS); - - // Detail - const detailContainer = dom.append(row2, $('.quick-input-list-label-meta')); - data.detail = new IconLabel(detailContainer, { supportHighlights: true, supportIcons: true, hoverDelegate: this.hoverDelegate }); - data.toDisposeTemplate.push(data.detail); - - // Separator - data.separator = dom.append(data.entry, $('.quick-input-list-separator')); - - // Actions - data.actionBar = new ActionBar(data.entry, this.hoverDelegate ? { hoverDelegate: this.hoverDelegate } : undefined); - data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar'); - data.toDisposeTemplate.push(data.actionBar); - - return data; - } - - renderElement(element: IListElement, index: number, data: IListElementTemplateData): void { - data.element = element; - element.element = data.entry ?? undefined; - const mainItem: QuickPickItem = element.item ? element.item : element.separator!; - - data.checkbox.checked = element.checked; - data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked)); - - const { labelHighlights, descriptionHighlights, detailHighlights } = element; - - if (element.item?.iconPath) { - const icon = isDark(this.themeService.getColorTheme().type) ? element.item.iconPath.dark : (element.item.iconPath.light ?? element.item.iconPath.dark); - const iconUrl = URI.revive(icon); - data.icon.className = 'quick-input-list-icon'; - data.icon.style.backgroundImage = dom.asCSSUrl(iconUrl); - } else { - data.icon.style.backgroundImage = ''; - data.icon.className = element.item?.iconClass ? `quick-input-list-icon ${element.item.iconClass}` : ''; - } - - // Label - let descriptionTitle: ITooltipMarkdownString | undefined; - // if we have a tooltip, that will be the hover, - // with the saneDescription as fallback if it - // is defined - if (!element.saneTooltip && element.saneDescription) { - descriptionTitle = { - markdown: { - value: element.saneDescription, - supportThemeIcons: true - }, - markdownNotSupportedFallback: element.saneDescription - }; - } - const options: IIconLabelValueOptions = { - matches: labelHighlights || [], - // If we have a tooltip, we want that to be shown and not any other hover - descriptionTitle, - descriptionMatches: descriptionHighlights || [], - labelEscapeNewLines: true - }; - if (mainItem.type !== 'separator') { - options.extraClasses = mainItem.iconClasses; - options.italic = mainItem.italic; - options.strikethrough = mainItem.strikethrough; - data.entry.classList.remove('quick-input-list-separator-as-item'); - } else { - data.entry.classList.add('quick-input-list-separator-as-item'); - } - data.label.setLabel(element.saneLabel, element.saneDescription, options); - - // Keybinding - data.keybinding.set(mainItem.type === 'separator' ? undefined : mainItem.keybinding); - - // Detail - if (element.saneDetail) { - let title: ITooltipMarkdownString | undefined; - // If we have a tooltip, we want that to be shown and not any other hover - if (!element.saneTooltip) { - title = { - markdown: { - value: element.saneDetail, - supportThemeIcons: true - }, - markdownNotSupportedFallback: element.saneDetail - }; - } - data.detail.element.style.display = ''; - data.detail.setLabel(element.saneDetail, undefined, { - matches: detailHighlights, - title, - labelEscapeNewLines: true - }); - } else { - data.detail.element.style.display = 'none'; - } - - // Separator - if (element.item && element.separator && element.separator.label) { - data.separator.textContent = element.separator.label; - data.separator.style.display = ''; - } else { - data.separator.style.display = 'none'; - } - data.entry.classList.toggle('quick-input-list-separator-border', !!element.separator); - - // Actions - const buttons = mainItem.buttons; - if (buttons && buttons.length) { - data.actionBar.push(buttons.map((button, index) => quickInputButtonToAction( - button, - `id-${index}`, - () => mainItem.type !== 'separator' - ? element.fireButtonTriggered({ button, item: mainItem }) - : element.fireSeparatorButtonTriggered({ button, separator: mainItem }) - )), { icon: true, label: false }); - data.entry.classList.add('has-actions'); - } else { - data.entry.classList.remove('has-actions'); - } - } - - disposeElement(element: IListElement, index: number, data: IListElementTemplateData): void { - data.toDisposeElement = dispose(data.toDisposeElement); - data.actionBar.clear(); - } - - disposeTemplate(data: IListElementTemplateData): void { - data.toDisposeElement = dispose(data.toDisposeElement); - data.toDisposeTemplate = dispose(data.toDisposeTemplate); - } -} - -class ListElementDelegate implements IListVirtualDelegate { - - getHeight(element: IListElement): number { - if (!element.item) { - // must be a separator - return 24; - } - return element.saneDetail ? 44 : 22; - } - - getTemplateId(element: IListElement): string { - return ListElementRenderer.ID; - } -} - -export enum QuickInputListFocus { - First = 1, - Second, - Last, - Next, - Previous, - NextPage, - PreviousPage -} - -export class QuickInputList { - - readonly id: string; - private container: HTMLElement; - private list: List; - private inputElements: Array = []; - private elements: IListElement[] = []; - private elementsToIndexes = new Map(); - matchOnDescription = false; - matchOnDetail = false; - matchOnLabel = true; - matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy'; - matchOnMeta = true; - sortByLabel = true; - private readonly _onChangedAllVisibleChecked = new Emitter(); - onChangedAllVisibleChecked: Event = this._onChangedAllVisibleChecked.event; - private readonly _onChangedCheckedCount = new Emitter(); - onChangedCheckedCount: Event = this._onChangedCheckedCount.event; - private readonly _onChangedVisibleCount = new Emitter(); - onChangedVisibleCount: Event = this._onChangedVisibleCount.event; - private readonly _onChangedCheckedElements = new Emitter(); - onChangedCheckedElements: Event = this._onChangedCheckedElements.event; - private readonly _onButtonTriggered = new Emitter>(); - onButtonTriggered = this._onButtonTriggered.event; - private readonly _onSeparatorButtonTriggered = new Emitter(); - onSeparatorButtonTriggered = this._onSeparatorButtonTriggered.event; - private readonly _onKeyDown = new Emitter(); - onKeyDown: Event = this._onKeyDown.event; - private readonly _onLeave = new Emitter(); - onLeave: Event = this._onLeave.event; - private readonly _listElementChecked = new Emitter<{ listElement: IListElement; checked: boolean }>(); - private _fireCheckedEvents = true; - private elementDisposables: IDisposable[] = []; - private disposables: IDisposable[] = []; - private _lastHover: IHoverWidget | undefined; - private _toggleHover: IDisposable | undefined; - - constructor( - private parent: HTMLElement, - id: string, - private options: IQuickInputOptions, - themeService: IThemeService - ) { - this.id = id; - this.container = dom.append(this.parent, $('.quick-input-list')); - const delegate = new ListElementDelegate(); - const accessibilityProvider = new QuickInputAccessibilityProvider(); - this.list = options.createList('QuickInput', this.container, delegate, [new ListElementRenderer(themeService, options.hoverDelegate)], { - identityProvider: { - getId: element => { - // always prefer item over separator because if item is defined, it must be the main item type - // always prefer a defined id if one was specified and use label as a fallback - return element.item?.id - ?? element.item?.label - ?? element.separator?.id - ?? element.separator?.label - ?? ''; - } - }, - setRowLineHeight: false, - multipleSelectionSupport: false, - horizontalScrolling: false, - accessibilityProvider - } as IListOptions); - this.list.getHTMLElement().id = id; - this.disposables.push(this.list); - this.disposables.push(this.list.onKeyDown(e => { - const event = new StandardKeyboardEvent(e); - switch (event.keyCode) { - case KeyCode.Space: - this.toggleCheckbox(); - break; - case KeyCode.KeyA: - if (platform.isMacintosh ? e.metaKey : e.ctrlKey) { - this.list.setFocus(range(this.list.length)); - } - break; - case KeyCode.UpArrow: { - const focus1 = this.list.getFocus(); - if (focus1.length === 1 && focus1[0] === 0) { - this._onLeave.fire(); - } - break; - } - case KeyCode.DownArrow: { - const focus2 = this.list.getFocus(); - if (focus2.length === 1 && focus2[0] === this.list.length - 1) { - this._onLeave.fire(); - } - break; - } - } - - this._onKeyDown.fire(event); - })); - this.disposables.push(this.list.onMouseDown(e => { - if (e.browserEvent.button !== 2) { - // Works around / fixes #64350. - e.browserEvent.preventDefault(); - } - })); - this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => { - if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox. - this._onLeave.fire(); - } - })); - this.disposables.push(this.list.onMouseMiddleClick(e => { - this._onLeave.fire(); - })); - this.disposables.push(this.list.onContextMenu(e => { - if (typeof e.index === 'number') { - e.browserEvent.preventDefault(); - - // we want to treat a context menu event as - // a gesture to open the item at the index - // since we do not have any context menu - // this enables for example macOS to Ctrl- - // click on an item to open it. - this.list.setSelection([e.index]); - } - })); - - const delayer = new ThrottledDelayer(options.hoverDelegate.delay); - // onMouseOver triggers every time a new element has been moused over - // even if it's on the same list item. - this.disposables.push(this.list.onMouseOver(async e => { - // If we hover over an anchor element, we don't want to show the hover because - // the anchor may have a tooltip that we want to show instead. - if (e.browserEvent.target instanceof HTMLAnchorElement) { - delayer.cancel(); - return; - } - if ( - // anchors are an exception as called out above so we skip them here - !(e.browserEvent.relatedTarget instanceof HTMLAnchorElement) && - // check if the mouse is still over the same element - dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node) - ) { - return; - } - try { - await delayer.trigger(async () => { - if (e.element) { - this.showHover(e.element); - } - }); - } catch (e) { - // Ignore cancellation errors due to mouse out - if (!isCancellationError(e)) { - throw e; - } - } - })); - this.disposables.push(this.list.onMouseOut(e => { - // onMouseOut triggers every time a new element has been moused over - // even if it's on the same list item. We only want one event, so we - // check if the mouse is still over the same element. - if (dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node)) { - return; - } - delayer.cancel(); - })); - this.disposables.push(delayer); - this.disposables.push(this._listElementChecked.event(_ => this.fireCheckedEvents())); - this.disposables.push( - this._onChangedAllVisibleChecked, - this._onChangedCheckedCount, - this._onChangedVisibleCount, - this._onChangedCheckedElements, - this._onButtonTriggered, - this._onSeparatorButtonTriggered, - this._onLeave, - this._onKeyDown - ); - } - - @memoize - get onDidChangeFocus() { - return Event.map(this.list.onDidChangeFocus, e => e.elements.map(e => e.item)); - } - - @memoize - get onDidChangeSelection() { - return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent })); - } - - get scrollTop() { - return this.list.scrollTop; - } - - set scrollTop(scrollTop: number) { - this.list.scrollTop = scrollTop; - } - - get ariaLabel() { - return this.list.getHTMLElement().ariaLabel; - } - - set ariaLabel(label: string | null) { - this.list.getHTMLElement().ariaLabel = label; - } - - getAllVisibleChecked() { - return this.allVisibleChecked(this.elements, false); - } - - private allVisibleChecked(elements: IListElement[], whenNoneVisible = true) { - for (let i = 0, n = elements.length; i < n; i++) { - const element = elements[i]; - if (!element.hidden) { - if (!element.checked) { - return false; - } else { - whenNoneVisible = true; - } - } - } - return whenNoneVisible; - } - - getCheckedCount() { - let count = 0; - const elements = this.elements; - for (let i = 0, n = elements.length; i < n; i++) { - if (elements[i].checked) { - count++; - } - } - return count; - } - - getVisibleCount() { - let count = 0; - const elements = this.elements; - for (let i = 0, n = elements.length; i < n; i++) { - if (!elements[i].hidden) { - count++; - } - } - return count; - } - - setAllVisibleChecked(checked: boolean) { - try { - this._fireCheckedEvents = false; - this.elements.forEach(element => { - if (!element.hidden) { - element.checked = checked; - } - }); - } finally { - this._fireCheckedEvents = true; - this.fireCheckedEvents(); - } - } - - setElements(inputElements: Array): void { - this.elementDisposables = dispose(this.elementDisposables); - const fireButtonTriggered = (event: IQuickPickItemButtonEvent) => this.fireButtonTriggered(event); - const fireSeparatorButtonTriggered = (event: IQuickPickSeparatorButtonEvent) => this.fireSeparatorButtonTriggered(event); - this.inputElements = inputElements; - const elementsToIndexes = new Map(); - const hasCheckbox = this.parent.classList.contains('show-checkboxes'); - this.elements = inputElements.reduce((result, item, index) => { - const previous = index > 0 ? inputElements[index - 1] : undefined; - if (item.type === 'separator') { - if (!item.buttons) { - // This separator will be rendered as a part of the list item - return result; - } - } - - const element = new ListElement( - item, - previous, - index, - hasCheckbox, - fireButtonTriggered, - fireSeparatorButtonTriggered, - this._listElementChecked - ); - - const resultIndex = result.length; - result.push(element); - elementsToIndexes.set(element.item ?? element.separator!, resultIndex); - return result; - }, [] as IListElement[]); - this.elementsToIndexes = elementsToIndexes; - this.list.splice(0, this.list.length); // Clear focus and selection first, sending the events when the list is empty. - this.list.splice(0, this.list.length, this.elements); - this._onChangedVisibleCount.fire(this.elements.length); - } - - getElementsCount(): number { - return this.inputElements.length; - } - - getFocusedElements() { - return this.list.getFocusedElements() - .map(e => e.item); - } - - setFocusedElements(items: IQuickPickItem[]) { - this.list.setFocus(items - .filter(item => this.elementsToIndexes.has(item)) - .map(item => this.elementsToIndexes.get(item)!)); - if (items.length > 0) { - const focused = this.list.getFocus()[0]; - if (typeof focused === 'number') { - this.list.reveal(focused); - } - } - } - - getActiveDescendant() { - return this.list.getHTMLElement().getAttribute('aria-activedescendant'); - } - - getSelectedElements() { - return this.list.getSelectedElements() - .map(e => e.item); - } - - setSelectedElements(items: IQuickPickItem[]) { - this.list.setSelection(items - .filter(item => this.elementsToIndexes.has(item)) - .map(item => this.elementsToIndexes.get(item)!)); - } - - getCheckedElements() { - return this.elements.filter(e => e.checked) - .map(e => e.item) - .filter(e => !!e) as IQuickPickItem[]; - } - - setCheckedElements(items: IQuickPickItem[]) { - try { - this._fireCheckedEvents = false; - const checked = new Set(); - for (const item of items) { - checked.add(item); - } - for (const element of this.elements) { - element.checked = checked.has(element.item); - } - } finally { - this._fireCheckedEvents = true; - this.fireCheckedEvents(); - } - } - - set enabled(value: boolean) { - this.list.getHTMLElement().style.pointerEvents = value ? '' : 'none'; - } - - focus(what: QuickInputListFocus): void { - if (!this.list.length) { - return; - } - - if (what === QuickInputListFocus.Second && this.list.length < 2) { - what = QuickInputListFocus.First; - } - - switch (what) { - case QuickInputListFocus.First: - this.list.scrollTop = 0; - this.list.focusFirst(undefined, (e) => !!e.item); - break; - case QuickInputListFocus.Second: - this.list.scrollTop = 0; - this.list.focusNth(1, undefined, (e) => !!e.item); - break; - case QuickInputListFocus.Last: - this.list.scrollTop = this.list.scrollHeight; - this.list.focusLast(undefined, (e) => !!e.item); - break; - case QuickInputListFocus.Next: { - this.list.focusNext(undefined, true, undefined, (e) => !!e.item); - const index = this.list.getFocus()[0]; - if (index !== 0 && !this.elements[index - 1].item && this.list.firstVisibleIndex > index - 1) { - this.list.reveal(index - 1); - } - break; - } - case QuickInputListFocus.Previous: { - this.list.focusPrevious(undefined, true, undefined, (e) => !!e.item); - const index = this.list.getFocus()[0]; - if (index !== 0 && !this.elements[index - 1].item && this.list.firstVisibleIndex > index - 1) { - this.list.reveal(index - 1); - } - break; - } - case QuickInputListFocus.NextPage: - this.list.focusNextPage(undefined, (e) => !!e.item); - break; - case QuickInputListFocus.PreviousPage: - this.list.focusPreviousPage(undefined, (e) => !!e.item); - break; - } - - const focused = this.list.getFocus()[0]; - if (typeof focused === 'number') { - this.list.reveal(focused); - } - } - - clearFocus() { - this.list.setFocus([]); - } - - domFocus() { - this.list.domFocus(); - } - - /** - * Disposes of the hover and shows a new one for the given index if it has a tooltip. - * @param element The element to show the hover for - */ - private showHover(element: IListElement): void { - if (this._lastHover && !this._lastHover.isDisposed) { - this.options.hoverDelegate.onDidHideHover?.(); - this._lastHover?.dispose(); - } - - if (!element.element || !element.saneTooltip) { - return; - } - this._lastHover = this.options.hoverDelegate.showHover({ - content: element.saneTooltip, - target: element.element, - linkHandler: (url) => { - this.options.linkOpenerDelegate(url); - }, - appearance: { - showPointer: true, - }, - container: this.container, - position: { - hoverPosition: HoverPosition.RIGHT - } - }, false); - } - - layout(maxHeight?: number): void { - this.list.getHTMLElement().style.maxHeight = maxHeight ? `${ - // Make sure height aligns with list item heights - Math.floor(maxHeight / 44) * 44 - // Add some extra height so that it's clear there's more to scroll - + 6 - }px` : ''; - this.list.layout(); - } - - filter(query: string): boolean { - if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { - this.list.layout(); - return false; - } - - const queryWithWhitespace = query; - query = query.trim(); - - // Reset filtering - if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { - this.elements.forEach(element => { - element.labelHighlights = undefined; - element.descriptionHighlights = undefined; - element.detailHighlights = undefined; - element.hidden = false; - const previous = element.index && this.inputElements[element.index - 1]; - if (element.item) { - element.separator = previous && previous.type === 'separator' && !previous.buttons ? previous : undefined; - } - }); - } - - // Filter by value (since we support icons in labels, use $(..) aware fuzzy matching) - else { - let currentSeparator: IQuickPickSeparator | undefined; - this.elements.forEach(element => { - let labelHighlights: IMatch[] | undefined; - if (this.matchOnLabelMode === 'fuzzy') { - labelHighlights = this.matchOnLabel ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel)) ?? undefined : undefined; - } else { - labelHighlights = this.matchOnLabel ? matchesContiguousIconAware(queryWithWhitespace, parseLabelWithIcons(element.saneLabel)) ?? undefined : undefined; - } - const descriptionHighlights = this.matchOnDescription ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || '')) ?? undefined : undefined; - const detailHighlights = this.matchOnDetail ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDetail || '')) ?? undefined : undefined; - - if (labelHighlights || descriptionHighlights || detailHighlights) { - element.labelHighlights = labelHighlights; - element.descriptionHighlights = descriptionHighlights; - element.detailHighlights = detailHighlights; - element.hidden = false; - } else { - element.labelHighlights = undefined; - element.descriptionHighlights = undefined; - element.detailHighlights = undefined; - element.hidden = element.item ? !element.item.alwaysShow : true; - } - - // Ensure separators are filtered out first before deciding if we need to bring them back - if (element.item) { - element.separator = undefined; - } else if (element.separator) { - element.hidden = true; - } - - // we can show the separator unless the list gets sorted by match - if (!this.sortByLabel) { - const previous = element.index && this.inputElements[element.index - 1]; - currentSeparator = previous && previous.type === 'separator' ? previous : currentSeparator; - if (currentSeparator && !element.hidden) { - element.separator = currentSeparator; - currentSeparator = undefined; - } - } - }); - } - - const shownElements = this.elements.filter(element => !element.hidden); - - // Sort by value - if (this.sortByLabel && query) { - const normalizedSearchValue = query.toLowerCase(); - shownElements.sort((a, b) => { - return compareEntries(a, b, normalizedSearchValue); - }); - } - - this.elementsToIndexes = shownElements.reduce((map, element, index) => { - map.set(element.item ?? element.separator!, index); - return map; - }, new Map()); - this.list.splice(0, this.list.length, shownElements); - this.list.setFocus([]); - this.list.layout(); - - this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); - this._onChangedVisibleCount.fire(shownElements.length); - - return true; - } - - toggleCheckbox() { - try { - this._fireCheckedEvents = false; - const elements = this.list.getFocusedElements(); - const allChecked = this.allVisibleChecked(elements); - for (const element of elements) { - element.checked = !allChecked; - } - } finally { - this._fireCheckedEvents = true; - this.fireCheckedEvents(); - } - } - - display(display: boolean) { - this.container.style.display = display ? '' : 'none'; - } - - isDisplayed() { - return this.container.style.display !== 'none'; - } - - dispose() { - this.elementDisposables = dispose(this.elementDisposables); - this.disposables = dispose(this.disposables); - } - - private fireCheckedEvents() { - if (this._fireCheckedEvents) { - this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); - this._onChangedCheckedCount.fire(this.getCheckedCount()); - this._onChangedCheckedElements.fire(this.getCheckedElements()); - } - } - - private fireButtonTriggered(event: IQuickPickItemButtonEvent) { - this._onButtonTriggered.fire(event); - } - - private fireSeparatorButtonTriggered(event: IQuickPickSeparatorButtonEvent) { - this._onSeparatorButtonTriggered.fire(event); - } - - style(styles: IListStyles) { - this.list.style(styles); - } - - toggleHover() { - const element: IListElement | undefined = this.list.getFocusedElements()[0]; - if (!element?.saneTooltip) { - return; - } - - // if there's a hover already, hide it (toggle off) - if (this._lastHover && !this._lastHover.isDisposed) { - this._lastHover.dispose(); - return; - } - - // If there is no hover, show it (toggle on) - const focused = this.list.getFocusedElements()[0]; - if (!focused) { - return; - } - this.showHover(focused); - const store = new DisposableStore(); - store.add(this.list.onDidChangeFocus(e => { - if (e.indexes.length) { - this.showHover(e.elements[0]); - } - })); - if (this._lastHover) { - store.add(this._lastHover); - } - this._toggleHover = store; - this.elementDisposables.push(this._toggleHover); - } -} - -function matchesContiguousIconAware(query: string, target: IParsedLabelWithIcons): IMatch[] | null { - - const { text, iconOffsets } = target; - - // Return early if there are no icon markers in the word to match against - if (!iconOffsets || iconOffsets.length === 0) { - return matchesContiguous(query, text); - } - - // Trim the word to match against because it could have leading - // whitespace now if the word started with an icon - const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' '); - const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length; - - // match on value without icon - const matches = matchesContiguous(query, wordToMatchAgainstWithoutIconsTrimmed); - - // Map matches back to offsets with icon and trimming - if (matches) { - for (const match of matches) { - const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */; - match.start += iconOffset; - match.end += iconOffset; - } - } - - return matches; -} - -function matchesContiguous(word: string, wordToMatchAgainst: string): IMatch[] | null { - const matchIndex = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase()); - if (matchIndex !== -1) { - return [{ start: matchIndex, end: matchIndex + word.length }]; - } - return null; -} - -function compareEntries(elementA: IListElement, elementB: IListElement, lookFor: string): number { - - const labelHighlightsA = elementA.labelHighlights || []; - const labelHighlightsB = elementB.labelHighlights || []; - if (labelHighlightsA.length && !labelHighlightsB.length) { - return -1; - } - - if (!labelHighlightsA.length && labelHighlightsB.length) { - return 1; - } - - if (labelHighlightsA.length === 0 && labelHighlightsB.length === 0) { - return 0; - } - - return compareAnything(elementA.saneSortLabel, elementB.saneSortLabel, lookFor); -} - -class QuickInputAccessibilityProvider implements IListAccessibilityProvider { - - getWidgetAriaLabel(): string { - return localize('quickInput', "Quick Input"); - } - - getAriaLabel(element: IListElement): string | null { - return element.separator?.label - ? `${element.saneAriaLabel}, ${element.separator.label}` - : element.saneAriaLabel; - } - - getWidgetRole(): AriaRole { - return 'listbox'; - } - - getRole(element: IListElement) { - return element.hasCheckbox ? 'checkbox' : 'option'; - } - - isChecked(element: IListElement) { - if (!element.hasCheckbox) { - return undefined; - } - - return { - value: element.checked, - onDidChange: element.onChecked - }; - } -} diff --git a/code/src/vs/platform/quickinput/browser/quickInputService.ts b/code/src/vs/platform/quickinput/browser/quickInputService.ts index 2974eed8077..e23c1158e68 100644 --- a/code/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/code/src/vs/platform/quickinput/browser/quickInputService.ts @@ -3,14 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { List } from 'vs/base/browser/ui/list/listWidget'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { QuickAccessController } from 'vs/platform/quickinput/browser/quickAccess'; import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; @@ -82,23 +79,16 @@ export class QuickInputService extends Themable implements IQuickInputService { }); }, returnFocus: () => host.focus(), - createList: ( - user: string, - container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], - options: IWorkbenchListOptions - ) => this.instantiationService.createInstance(WorkbenchList, user, container, delegate, renderers, options) as List, styles: this.computeStyles(), hoverDelegate: this._register(this.instantiationService.createInstance(QuickInputHoverDelegate)) }; - const controller = this._register(new QuickInputController({ - ...defaultOptions, - ...options - }, - this.themeService, - this.layoutService + const controller = this._register(this.instantiationService.createInstance( + QuickInputController, + { + ...defaultOptions, + ...options + } )); controller.layout(host.activeContainerDimension, host.activeContainerOffset.quickPickTop); diff --git a/code/src/vs/platform/quickinput/browser/quickInputTree.ts b/code/src/vs/platform/quickinput/browser/quickInputTree.ts new file mode 100644 index 00000000000..2deb1f501cf --- /dev/null +++ b/code/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -0,0 +1,1651 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter, Event } from 'vs/base/common/event'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IObjectTreeElement, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMatch } from 'vs/base/common/filters'; +import { IListAccessibilityProvider, IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { OS, isMacintosh } from 'vs/base/common/platform'; +import { memoize } from 'vs/base/common/decorators'; +import { IIconLabelValueOptions, IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { isDark } from 'vs/platform/theme/common/theme'; +import { URI } from 'vs/base/common/uri'; +import { IHoverWidget, ITooltipMarkdownString } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { quickInputButtonToAction } from 'vs/platform/quickinput/browser/quickInputUtils'; +import { Lazy } from 'vs/base/common/lazy'; +import { IParsedLabelWithIcons, getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { compareAnything } from 'vs/base/common/comparers'; +import { ltrim } from 'vs/base/common/strings'; +import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { isCancellationError } from 'vs/base/common/errors'; + +const $ = dom.$; + +export enum QuickInputListFocus { + First = 1, + Second, + Last, + Next, + Previous, + NextPage, + PreviousPage, + NextSeparator, + PreviousSeparator +} + +interface IQuickInputItemLazyParts { + readonly saneLabel: string; + readonly saneSortLabel: string; + readonly saneAriaLabel: string; +} + +interface IQuickPickElement extends IQuickInputItemLazyParts { + readonly hasCheckbox: boolean; + readonly index: number; + readonly item?: IQuickPickItem; + readonly saneDescription?: string; + readonly saneDetail?: string; + readonly saneTooltip?: string | IMarkdownString | HTMLElement; + readonly onChecked: Event; + checked: boolean; + hidden: boolean; + element?: HTMLElement; + labelHighlights?: IMatch[]; + descriptionHighlights?: IMatch[]; + detailHighlights?: IMatch[]; + separator?: IQuickPickSeparator; +} + +interface IQuickInputItemTemplateData { + entry: HTMLDivElement; + checkbox: HTMLInputElement; + icon: HTMLDivElement; + label: IconLabel; + keybinding: KeybindingLabel; + detail: IconLabel; + separator: HTMLDivElement; + actionBar: ActionBar; + element: IQuickPickElement; + toDisposeElement: DisposableStore; + toDisposeTemplate: DisposableStore; +} + +class BaseQuickPickItemElement implements IQuickPickElement { + private readonly _init: Lazy; + + readonly onChecked: Event; + + constructor( + readonly index: number, + readonly hasCheckbox: boolean, + private _onChecked: Emitter<{ element: IQuickPickElement; checked: boolean }>, + mainItem: QuickPickItem + ) { + this.onChecked = hasCheckbox + ? Event.map(Event.filter<{ element: IQuickPickElement; checked: boolean }>(this._onChecked.event, e => e.element === this), e => e.checked) + : Event.None; + this._init = new Lazy(() => { + const saneLabel = mainItem.label ?? ''; + const saneSortLabel = parseLabelWithIcons(saneLabel).text.trim(); + + const saneAriaLabel = mainItem.ariaLabel || [saneLabel, this.saneDescription, this.saneDetail] + .map(s => getCodiconAriaLabel(s)) + .filter(s => !!s) + .join(', '); + + return { + saneLabel, + saneSortLabel, + saneAriaLabel + }; + }); + } + + // #region Lazy Getters + + get saneLabel() { + return this._init.value.saneLabel; + } + get saneSortLabel() { + return this._init.value.saneSortLabel; + } + get saneAriaLabel() { + return this._init.value.saneAriaLabel; + } + + // #endregion + + // #region Getters and Setters + + private _element?: HTMLElement; + get element() { + return this._element; + } + set element(value: HTMLElement | undefined) { + this._element = value; + } + + private _hidden: boolean = false; + get hidden() { + return this._hidden; + } + set hidden(value: boolean) { + this._hidden = value; + } + + private _checked: boolean = false; + get checked() { + return this._checked; + } + set checked(value: boolean) { + if (value !== this._checked) { + this._checked = value; + this._onChecked.fire({ element: this, checked: value }); + } + } + + protected _saneDescription?: string; + get saneDescription() { + return this._saneDescription; + } + set saneDescription(value: string | undefined) { + this._saneDescription = value; + } + + protected _saneDetail?: string; + get saneDetail() { + return this._saneDetail; + } + set saneDetail(value: string | undefined) { + this._saneDetail = value; + } + + protected _saneTooltip?: string | IMarkdownString | HTMLElement; + get saneTooltip() { + return this._saneTooltip; + } + set saneTooltip(value: string | IMarkdownString | HTMLElement | undefined) { + this._saneTooltip = value; + } + + protected _labelHighlights?: IMatch[]; + get labelHighlights() { + return this._labelHighlights; + } + set labelHighlights(value: IMatch[] | undefined) { + this._labelHighlights = value; + } + + protected _descriptionHighlights?: IMatch[]; + get descriptionHighlights() { + return this._descriptionHighlights; + } + set descriptionHighlights(value: IMatch[] | undefined) { + this._descriptionHighlights = value; + } + + protected _detailHighlights?: IMatch[]; + get detailHighlights() { + return this._detailHighlights; + } + set detailHighlights(value: IMatch[] | undefined) { + this._detailHighlights = value; + } +} + +class QuickPickItemElement extends BaseQuickPickItemElement { + constructor( + index: number, + hasCheckbox: boolean, + readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void, + onCheckedEmitter: Emitter<{ element: IQuickPickElement; checked: boolean }>, + readonly item: IQuickPickItem, + private _separator: IQuickPickSeparator | undefined, + ) { + super(index, hasCheckbox, onCheckedEmitter, item); + this._saneDescription = item.description; + this._saneDetail = item.detail; + this._saneTooltip = this.item.tooltip; + this._labelHighlights = item.highlights?.label; + this._descriptionHighlights = item.highlights?.description; + this._detailHighlights = item.highlights?.detail; + } + + get separator() { + return this._separator; + } + set separator(value: IQuickPickSeparator | undefined) { + this._separator = value; + } +} + +enum QuickPickSeparatorFocusReason { + /** + * No item is hovered or active + */ + NONE = 0, + /** + * Some item within this section is hovered + */ + MOUSE_HOVER = 1, + /** + * Some item within this section is active + */ + ACTIVE_ITEM = 2 +} + +class QuickPickSeparatorElement extends BaseQuickPickItemElement { + children = new Array(); + /** + * If this item is >0, it means that there is some item in the list that is either: + * * hovered over + * * active + */ + focusInsideSeparator = QuickPickSeparatorFocusReason.NONE; + + constructor( + index: number, + readonly fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void, + // TODO: remove this + onCheckedEmitter: Emitter<{ element: IQuickPickElement; checked: boolean }>, + readonly separator: IQuickPickSeparator, + ) { + super(index, false, onCheckedEmitter, separator); + } +} + +class QuickInputItemDelegate implements IListVirtualDelegate { + getHeight(element: IQuickPickElement): number { + + if (!element.item) { + // must be a separator + return 24; + } + return element.saneDetail ? 44 : 22; + } + + getTemplateId(element: IQuickPickElement): string { + if (element instanceof QuickPickItemElement) { + return QuickPickItemElementRenderer.ID; + } else { + return QuickPickSeparatorElementRenderer.ID; + } + } +} + +class QuickInputAccessibilityProvider implements IListAccessibilityProvider { + + getWidgetAriaLabel(): string { + return localize('quickInput', "Quick Input"); + } + + getAriaLabel(element: IQuickPickElement): string | null { + return element.separator?.label + ? `${element.saneAriaLabel}, ${element.separator.label}` + : element.saneAriaLabel; + } + + getWidgetRole(): AriaRole { + return 'listbox'; + } + + getRole(element: IQuickPickElement) { + return element.hasCheckbox ? 'checkbox' : 'option'; + } + + isChecked(element: IQuickPickElement) { + if (!element.hasCheckbox) { + return undefined; + } + + return { + value: element.checked, + onDidChange: element.onChecked + }; + } +} + +abstract class BaseQuickInputListRenderer implements ITreeRenderer { + abstract templateId: string; + + constructor( + private readonly hoverDelegate: IHoverDelegate | undefined + ) { } + + // TODO: only do the common stuff here and have a subclass handle their specific stuff + renderTemplate(container: HTMLElement): IQuickInputItemTemplateData { + const data: IQuickInputItemTemplateData = Object.create(null); + data.toDisposeElement = new DisposableStore(); + data.toDisposeTemplate = new DisposableStore(); + data.entry = dom.append(container, $('.quick-input-list-entry')); + + // Checkbox + const label = dom.append(data.entry, $('label.quick-input-list-label')); + data.toDisposeTemplate.add(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => { + if (!data.checkbox.offsetParent) { // If checkbox not visible: + e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740 + } + })); + data.checkbox = dom.append(label, $('input.quick-input-list-checkbox')); + data.checkbox.type = 'checkbox'; + data.toDisposeTemplate.add(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { + data.element.checked = data.checkbox.checked; + })); + + // Rows + const rows = dom.append(label, $('.quick-input-list-rows')); + const row1 = dom.append(rows, $('.quick-input-list-row')); + const row2 = dom.append(rows, $('.quick-input-list-row')); + + // Label + data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportIcons: true, hoverDelegate: this.hoverDelegate }); + data.toDisposeTemplate.add(data.label); + data.icon = dom.prepend(data.label.element, $('.quick-input-list-icon')); + + // Keybinding + const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding')); + data.keybinding = new KeybindingLabel(keybindingContainer, OS); + data.toDisposeTemplate.add(data.keybinding); + + // Detail + const detailContainer = dom.append(row2, $('.quick-input-list-label-meta')); + data.detail = new IconLabel(detailContainer, { supportHighlights: true, supportIcons: true, hoverDelegate: this.hoverDelegate }); + data.toDisposeTemplate.add(data.detail); + + // Separator + data.separator = dom.append(data.entry, $('.quick-input-list-separator')); + + // Actions + data.actionBar = new ActionBar(data.entry, this.hoverDelegate ? { hoverDelegate: this.hoverDelegate } : undefined); + data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar'); + data.toDisposeTemplate.add(data.actionBar); + + return data; + } + + disposeTemplate(data: IQuickInputItemTemplateData): void { + data.toDisposeElement.dispose(); + data.toDisposeTemplate.dispose(); + } + + disposeElement(_element: ITreeNode, _index: number, data: IQuickInputItemTemplateData): void { + data.toDisposeElement.clear(); + data.actionBar.clear(); + } + + // TODO: only do the common stuff here and have a subclass handle their specific stuff + abstract renderElement(node: ITreeNode, index: number, data: IQuickInputItemTemplateData): void; +} + +class QuickPickItemElementRenderer extends BaseQuickInputListRenderer { + static readonly ID = 'quickpickitem'; + + // Follow what we do in the separator renderer + private readonly _itemsWithSeparatorsFrequency = new Map(); + + constructor( + hoverDelegate: IHoverDelegate | undefined, + @IThemeService private readonly themeService: IThemeService, + ) { + super(hoverDelegate); + } + + get templateId() { + return QuickPickItemElementRenderer.ID; + } + + renderElement(node: ITreeNode, index: number, data: IQuickInputItemTemplateData): void { + const element = node.element; + data.element = element; + element.element = data.entry ?? undefined; + const mainItem: IQuickPickItem = element.item; + + data.checkbox.checked = element.checked; + data.toDisposeElement.add(element.onChecked(checked => data.checkbox.checked = checked)); + + const { labelHighlights, descriptionHighlights, detailHighlights } = element; + + if (mainItem.iconPath) { + const icon = isDark(this.themeService.getColorTheme().type) ? mainItem.iconPath.dark : (mainItem.iconPath.light ?? mainItem.iconPath.dark); + const iconUrl = URI.revive(icon); + data.icon.className = 'quick-input-list-icon'; + data.icon.style.backgroundImage = dom.asCSSUrl(iconUrl); + } else { + data.icon.style.backgroundImage = ''; + data.icon.className = mainItem.iconClass ? `quick-input-list-icon ${mainItem.iconClass}` : ''; + } + + // Label + let descriptionTitle: ITooltipMarkdownString | undefined; + // if we have a tooltip, that will be the hover, + // with the saneDescription as fallback if it + // is defined + if (!element.saneTooltip && element.saneDescription) { + descriptionTitle = { + markdown: { + value: element.saneDescription, + supportThemeIcons: true + }, + markdownNotSupportedFallback: element.saneDescription + }; + } + const options: IIconLabelValueOptions = { + matches: labelHighlights || [], + // If we have a tooltip, we want that to be shown and not any other hover + descriptionTitle, + descriptionMatches: descriptionHighlights || [], + labelEscapeNewLines: true + }; + options.extraClasses = mainItem.iconClasses; + options.italic = mainItem.italic; + options.strikethrough = mainItem.strikethrough; + data.entry.classList.remove('quick-input-list-separator-as-item'); + data.label.setLabel(element.saneLabel, element.saneDescription, options); + + // Keybinding + data.keybinding.set(mainItem.keybinding); + + // Detail + if (element.saneDetail) { + let title: ITooltipMarkdownString | undefined; + // If we have a tooltip, we want that to be shown and not any other hover + if (!element.saneTooltip) { + title = { + markdown: { + value: element.saneDetail, + supportThemeIcons: true + }, + markdownNotSupportedFallback: element.saneDetail + }; + } + data.detail.element.style.display = ''; + data.detail.setLabel(element.saneDetail, undefined, { + matches: detailHighlights, + title, + labelEscapeNewLines: true + }); + } else { + data.detail.element.style.display = 'none'; + } + + // Separator + if (element.separator?.label) { + data.separator.textContent = element.separator.label; + data.separator.style.display = ''; + this.addItemWithSeparator(element); + } else { + data.separator.style.display = 'none'; + } + data.entry.classList.toggle('quick-input-list-separator-border', !!element.separator); + + // Actions + const buttons = mainItem.buttons; + if (buttons && buttons.length) { + data.actionBar.push(buttons.map((button, index) => quickInputButtonToAction( + button, + `id-${index}`, + () => element.fireButtonTriggered({ button, item: element.item }) + )), { icon: true, label: false }); + data.entry.classList.add('has-actions'); + } else { + data.entry.classList.remove('has-actions'); + } + } + + override disposeElement(element: ITreeNode, _index: number, data: IQuickInputItemTemplateData): void { + this.removeItemWithSeparator(element.element); + super.disposeElement(element, _index, data); + } + + isItemWithSeparatorVisible(item: QuickPickItemElement): boolean { + return this._itemsWithSeparatorsFrequency.has(item); + } + + private addItemWithSeparator(item: QuickPickItemElement): void { + this._itemsWithSeparatorsFrequency.set(item, (this._itemsWithSeparatorsFrequency.get(item) || 0) + 1); + } + + private removeItemWithSeparator(item: QuickPickItemElement): void { + const frequency = this._itemsWithSeparatorsFrequency.get(item) || 0; + if (frequency > 1) { + this._itemsWithSeparatorsFrequency.set(item, frequency - 1); + } else { + this._itemsWithSeparatorsFrequency.delete(item); + } + } +} + +class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer { + static readonly ID = 'quickpickseparator'; + + // This is a frequency map because sticky scroll re-uses the same renderer to render a second + // instance of the same separator. + private readonly _visibleSeparatorsFrequency = new Map(); + + get templateId() { + return QuickPickSeparatorElementRenderer.ID; + } + + get visibleSeparators(): QuickPickSeparatorElement[] { + return [...this._visibleSeparatorsFrequency.keys()]; + } + + isSeparatorVisible(separator: QuickPickSeparatorElement): boolean { + return this._visibleSeparatorsFrequency.has(separator); + } + + override renderElement(node: ITreeNode, index: number, data: IQuickInputItemTemplateData): void { + const element = node.element; + data.element = element; + element.element = data.entry ?? undefined; + element.element.classList.toggle('focus-inside', !!element.focusInsideSeparator); + const mainItem: IQuickPickSeparator = element.separator; + + const { labelHighlights, descriptionHighlights, detailHighlights } = element; + + // Label + let descriptionTitle: ITooltipMarkdownString | undefined; + // if we have a tooltip, that will be the hover, + // with the saneDescription as fallback if it + // is defined + if (!element.saneTooltip && element.saneDescription) { + descriptionTitle = { + markdown: { + value: element.saneDescription, + supportThemeIcons: true + }, + markdownNotSupportedFallback: element.saneDescription + }; + } + const options: IIconLabelValueOptions = { + matches: labelHighlights || [], + // If we have a tooltip, we want that to be shown and not any other hover + descriptionTitle, + descriptionMatches: descriptionHighlights || [], + labelEscapeNewLines: true + }; + data.entry.classList.add('quick-input-list-separator-as-item'); + data.label.setLabel(element.saneLabel, element.saneDescription, options); + + // Detail + if (element.saneDetail) { + let title: ITooltipMarkdownString | undefined; + // If we have a tooltip, we want that to be shown and not any other hover + if (!element.saneTooltip) { + title = { + markdown: { + value: element.saneDetail, + supportThemeIcons: true + }, + markdownNotSupportedFallback: element.saneDetail + }; + } + data.detail.element.style.display = ''; + data.detail.setLabel(element.saneDetail, undefined, { + matches: detailHighlights, + title, + labelEscapeNewLines: true + }); + } else { + data.detail.element.style.display = 'none'; + } + + // Separator + data.separator.style.display = 'none'; + data.entry.classList.add('quick-input-list-separator-border'); + + // Actions + const buttons = mainItem.buttons; + if (buttons && buttons.length) { + data.actionBar.push(buttons.map((button, index) => quickInputButtonToAction( + button, + `id-${index}`, + () => element.fireSeparatorButtonTriggered({ button, separator: element.separator }) + )), { icon: true, label: false }); + data.entry.classList.add('has-actions'); + } else { + data.entry.classList.remove('has-actions'); + } + + this.addSeparator(element); + } + + override disposeElement(element: ITreeNode, _index: number, data: IQuickInputItemTemplateData): void { + this.removeSeparator(element.element); + if (!this.isSeparatorVisible(element.element)) { + element.element.element?.classList.remove('focus-inside'); + } + super.disposeElement(element, _index, data); + } + + private addSeparator(separator: QuickPickSeparatorElement): void { + this._visibleSeparatorsFrequency.set(separator, (this._visibleSeparatorsFrequency.get(separator) || 0) + 1); + } + + private removeSeparator(separator: QuickPickSeparatorElement): void { + const frequency = this._visibleSeparatorsFrequency.get(separator) || 0; + if (frequency > 1) { + this._visibleSeparatorsFrequency.set(separator, frequency - 1); + } else { + this._visibleSeparatorsFrequency.delete(separator); + } + } +} + +export class QuickInputTree extends Disposable { + + private readonly _onKeyDown = new Emitter(); + /** + * Event that is fired when the tree receives a keydown. + */ + readonly onKeyDown: Event = this._onKeyDown.event; + + private readonly _onLeave = new Emitter(); + /** + * Event that is fired when the tree would no longer have focus. + */ + readonly onLeave: Event = this._onLeave.event; + + private readonly _onChangedAllVisibleChecked = new Emitter(); + onChangedAllVisibleChecked: Event = this._onChangedAllVisibleChecked.event; + + private readonly _onChangedCheckedCount = new Emitter(); + onChangedCheckedCount: Event = this._onChangedCheckedCount.event; + + private readonly _onChangedVisibleCount = new Emitter(); + onChangedVisibleCount: Event = this._onChangedVisibleCount.event; + + private readonly _onChangedCheckedElements = new Emitter(); + onChangedCheckedElements: Event = this._onChangedCheckedElements.event; + + private readonly _onButtonTriggered = new Emitter>(); + onButtonTriggered = this._onButtonTriggered.event; + + private readonly _onSeparatorButtonTriggered = new Emitter(); + onSeparatorButtonTriggered = this._onSeparatorButtonTriggered.event; + + private readonly _container: HTMLElement; + private readonly _tree: WorkbenchObjectTree; + private readonly _separatorRenderer: QuickPickSeparatorElementRenderer; + private readonly _itemRenderer: QuickPickItemElementRenderer; + private readonly _elementChecked = new Emitter<{ element: IQuickPickElement; checked: boolean }>(); + private _inputElements = new Array(); + private _elementTree = new Array(); + private _itemElements = new Array(); + // Elements that apply to the current set of elements + private _elementDisposable = this._register(new DisposableStore()); + private _lastHover: IHoverWidget | undefined; + + constructor( + private parent: HTMLElement, + private hoverDelegate: IHoverDelegate, + private linkOpenerDelegate: (content: string) => void, + id: string, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(); + this._container = dom.append(this.parent, $('.quick-input-list')); + this._separatorRenderer = new QuickPickSeparatorElementRenderer(hoverDelegate); + this._itemRenderer = instantiationService.createInstance(QuickPickItemElementRenderer, hoverDelegate); + this._tree = this._register(instantiationService.createInstance( + WorkbenchObjectTree, + 'QuickInput', + this._container, + new QuickInputItemDelegate(), + [this._itemRenderer, this._separatorRenderer], + { + accessibilityProvider: new QuickInputAccessibilityProvider(), + setRowLineHeight: false, + multipleSelectionSupport: false, + hideTwistiesOfChildlessElements: true, + renderIndentGuides: RenderIndentGuides.None, + findWidgetEnabled: false, + indent: 0, + horizontalScrolling: false, + allowNonCollapsibleParents: true, + identityProvider: { + getId: element => { + // always prefer item over separator because if item is defined, it must be the main item type + // always prefer a defined id if one was specified and use label as a fallback + return element.item?.id + ?? element.item?.label + ?? element.separator?.id + ?? element.separator?.label + ?? ''; + } + }, + alwaysConsumeMouseWheel: true + } + )); + this._tree.getHTMLElement().id = id; + this._registerListeners(); + } + + //#region public getters/setters + + @memoize + get onDidChangeFocus() { + return Event.map( + this._tree.onDidChangeFocus, + e => e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item) + ); + } + + @memoize + get onDidChangeSelection() { + return Event.map( + this._tree.onDidChangeSelection, + e => ({ + items: e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item), + event: e.browserEvent + })); + } + + get scrollTop() { + return this._tree.scrollTop; + } + + set scrollTop(scrollTop: number) { + this._tree.scrollTop = scrollTop; + } + + get ariaLabel() { + return this._tree.ariaLabel; + } + + set ariaLabel(label: string | null) { + this._tree.ariaLabel = label ?? ''; + } + + set enabled(value: boolean) { + this._tree.getHTMLElement().style.pointerEvents = value ? '' : 'none'; + } + + private _matchOnDescription = false; + get matchOnDescription() { + return this._matchOnDescription; + } + set matchOnDescription(value: boolean) { + this._matchOnDescription = value; + } + + private _matchOnDetail = false; + get matchOnDetail() { + return this._matchOnDetail; + } + set matchOnDetail(value: boolean) { + this._matchOnDetail = value; + } + + private _matchOnLabel = true; + get matchOnLabel() { + return this._matchOnLabel; + } + set matchOnLabel(value: boolean) { + this._matchOnLabel = value; + } + + private _matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy'; + get matchOnLabelMode() { + return this._matchOnLabelMode; + } + set matchOnLabelMode(value: 'fuzzy' | 'contiguous') { + this._matchOnLabelMode = value; + } + + private _matchOnMeta = true; + get matchOnMeta() { + return this._matchOnMeta; + } + set matchOnMeta(value: boolean) { + this._matchOnMeta = value; + } + + private _sortByLabel = true; + get sortByLabel() { + return this._sortByLabel; + } + set sortByLabel(value: boolean) { + this._sortByLabel = value; + } + + //#endregion + + //#region register listeners + + private _registerListeners() { + this._registerOnKeyDown(); + this._registerOnContainerClick(); + this._registerOnMouseMiddleClick(); + this._registerOnElementChecked(); + this._registerOnContextMenu(); + this._registerHoverListeners(); + this._registerSelectionChangeListener(); + this._registerSeparatorActionShowingListeners(); + } + + private _registerOnKeyDown() { + // TODO: Should this be added at a higher level? + this._register(this._tree.onKeyDown(e => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Space: + this.toggleCheckbox(); + break; + case KeyCode.KeyA: + if (isMacintosh ? e.metaKey : e.ctrlKey) { + this._tree.setFocus(this._itemElements); + } + break; + // When we hit the top of the tree, we fire the onLeave event. + case KeyCode.UpArrow: { + const focus1 = this._tree.getFocus(); + if (focus1.length === 1 && focus1[0] === this._itemElements[0]) { + this._onLeave.fire(); + } + break; + } + // When we hit the bottom of the tree, we fire the onLeave event. + case KeyCode.DownArrow: { + const focus2 = this._tree.getFocus(); + if (focus2.length === 1 && focus2[0] === this._itemElements[this._itemElements.length - 1]) { + this._onLeave.fire(); + } + break; + } + } + + this._onKeyDown.fire(event); + })); + } + + private _registerOnContainerClick() { + this._register(dom.addDisposableListener(this._container, dom.EventType.CLICK, e => { + if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox. + this._onLeave.fire(); + } + })); + } + + private _registerOnMouseMiddleClick() { + this._register(dom.addDisposableListener(this._container, dom.EventType.AUXCLICK, e => { + if (e.button === 1) { + this._onLeave.fire(); + } + })); + } + + private _registerOnElementChecked() { + this._register(this._elementChecked.event(_ => this._fireCheckedEvents())); + } + + private _registerOnContextMenu() { + this._register(this._tree.onContextMenu(e => { + if (e.element) { + e.browserEvent.preventDefault(); + + // we want to treat a context menu event as + // a gesture to open the item at the index + // since we do not have any context menu + // this enables for example macOS to Ctrl- + // click on an item to open it. + this._tree.setSelection([e.element]); + } + })); + } + + private _registerHoverListeners() { + const delayer = this._register(new ThrottledDelayer(this.hoverDelegate.delay)); + this._register(this._tree.onMouseOver(async e => { + // If we hover over an anchor element, we don't want to show the hover because + // the anchor may have a tooltip that we want to show instead. + if (e.browserEvent.target instanceof HTMLAnchorElement) { + delayer.cancel(); + return; + } + if ( + // anchors are an exception as called out above so we skip them here + !(e.browserEvent.relatedTarget instanceof HTMLAnchorElement) && + // check if the mouse is still over the same element + dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node) + ) { + return; + } + try { + await delayer.trigger(async () => { + if (e.element instanceof QuickPickItemElement) { + this.showHover(e.element); + } + }); + } catch (e) { + // Ignore cancellation errors due to mouse out + if (!isCancellationError(e)) { + throw e; + } + } + })); + this._register(this._tree.onMouseOut(e => { + // onMouseOut triggers every time a new element has been moused over + // even if it's on the same list item. We only want one event, so we + // check if the mouse is still over the same element. + if (dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node)) { + return; + } + delayer.cancel(); + })); + } + + /** + * Register's focus change and mouse events so that we can track when items inside of a + * separator's section are focused or hovered so that we can display the separator's actions + */ + private _registerSeparatorActionShowingListeners() { + this._register(this._tree.onDidChangeFocus(e => { + const parent = e.elements[0] + ? this._tree.getParentElement(e.elements[0]) as QuickPickSeparatorElement + // treat null as focus lost and when we have no separators + : null; + for (const separator of this._separatorRenderer.visibleSeparators) { + const value = separator === parent; + // get bitness of ACTIVE_ITEM and check if it changed + const currentActive = !!(separator.focusInsideSeparator & QuickPickSeparatorFocusReason.ACTIVE_ITEM); + if (currentActive !== value) { + if (value) { + separator.focusInsideSeparator |= QuickPickSeparatorFocusReason.ACTIVE_ITEM; + } else { + separator.focusInsideSeparator &= ~QuickPickSeparatorFocusReason.ACTIVE_ITEM; + } + + this._tree.rerender(separator); + } + } + })); + this._register(this._tree.onMouseOver(e => { + const parent = e.element + ? this._tree.getParentElement(e.element) as QuickPickSeparatorElement + : null; + for (const separator of this._separatorRenderer.visibleSeparators) { + if (separator !== parent) { + continue; + } + const currentMouse = !!(separator.focusInsideSeparator & QuickPickSeparatorFocusReason.MOUSE_HOVER); + if (!currentMouse) { + separator.focusInsideSeparator |= QuickPickSeparatorFocusReason.MOUSE_HOVER; + this._tree.rerender(separator); + } + } + })); + this._register(this._tree.onMouseOut(e => { + const parent = e.element + ? this._tree.getParentElement(e.element) as QuickPickSeparatorElement + : null; + for (const separator of this._separatorRenderer.visibleSeparators) { + if (separator !== parent) { + continue; + } + const currentMouse = !!(separator.focusInsideSeparator & QuickPickSeparatorFocusReason.MOUSE_HOVER); + if (currentMouse) { + separator.focusInsideSeparator &= ~QuickPickSeparatorFocusReason.MOUSE_HOVER; + this._tree.rerender(separator); + } + } + })); + } + + private _registerSelectionChangeListener() { + // When the user selects a separator, the separator will move to the top and focus will be + // set to the first element after the separator. + this._register(this._tree.onDidChangeSelection(e => { + const elementsWithoutSeparators = e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement); + if (elementsWithoutSeparators.length !== e.elements.length) { + if (e.elements.length === 1 && e.elements[0] instanceof QuickPickSeparatorElement) { + this._tree.setFocus([e.elements[0].children[0]]); + this._tree.reveal(e.elements[0], 0); + } + this._tree.setSelection(elementsWithoutSeparators); + } + })); + } + + //#endregion + + //#region public methods + + getAllVisibleChecked() { + return this._allVisibleChecked(this._itemElements, false); + } + + getCheckedCount() { + return this._itemElements.filter(element => element.checked).length; + } + + getVisibleCount() { + return this._itemElements.filter(e => !e.hidden).length; + } + + setAllVisibleChecked(checked: boolean) { + this._itemElements.forEach(element => { + if (!element.hidden) { + element.checked = checked; + } + }); + this._fireCheckedEvents(); + } + + setElements(inputElements: QuickPickItem[]): void { + this._elementDisposable.clear(); + this._inputElements = inputElements; + const hasCheckbox = this.parent.classList.contains('show-checkboxes'); + let currentSeparatorElement: QuickPickSeparatorElement | undefined; + this._itemElements = new Array(); + this._elementTree = inputElements.reduce((result, item, index) => { + let element: IQuickPickElement; + if (item.type === 'separator') { + if (!item.buttons) { + // This separator will be rendered as a part of the list item + return result; + } + currentSeparatorElement = new QuickPickSeparatorElement( + index, + (event: IQuickPickSeparatorButtonEvent) => this.fireSeparatorButtonTriggered(event), + this._elementChecked, + item + ); + element = currentSeparatorElement; + } else { + const previous = index > 0 ? inputElements[index - 1] : undefined; + let separator: IQuickPickSeparator | undefined; + if (previous && previous.type === 'separator' && !previous.buttons) { + // Found an inline separator so we clear out the current separator element + currentSeparatorElement = undefined; + separator = previous; + } + const qpi = new QuickPickItemElement( + index, + hasCheckbox, + (event: IQuickPickItemButtonEvent) => this.fireButtonTriggered(event), + this._elementChecked, + item, + separator, + ); + this._itemElements.push(qpi); + + if (currentSeparatorElement) { + currentSeparatorElement.children.push(qpi); + return result; + } + element = qpi; + } + + result.push(element); + return result; + }, new Array()); + + const elements = new Array>(); + let visibleCount = 0; + for (const element of this._elementTree) { + if (element instanceof QuickPickSeparatorElement) { + elements.push({ + element, + collapsible: false, + collapsed: false, + children: element.children.map(e => ({ + element: e, + collapsible: false, + collapsed: false, + })), + }); + visibleCount += element.children.length + 1; // +1 for the separator itself; + } else { + elements.push({ + element, + collapsible: false, + collapsed: false, + }); + visibleCount++; + } + } + this._tree.setChildren(null, elements); + this._onChangedVisibleCount.fire(visibleCount); + } + + getElementsCount(): number { + return this._inputElements.length; + } + + getFocusedElements() { + return this._tree.getFocus() + .filter((e): e is IQuickPickElement => !!e) + .map(e => e.item) + .filter((e): e is IQuickPickItem => !!e); + } + + setFocusedElements(items: IQuickPickItem[]) { + const elements = items.map(item => this._itemElements.find(e => e.item === item)) + .filter((e): e is QuickPickItemElement => !!e); + this._tree.setFocus(elements); + if (items.length > 0) { + const focused = this._tree.getFocus()[0]; + if (focused) { + this._tree.reveal(focused); + } + } + } + + getActiveDescendant() { + return this._tree.getHTMLElement().getAttribute('aria-activedescendant'); + } + + getSelectedElements() { + return this._tree.getSelection() + .filter((e): e is IQuickPickElement => !!e && !!(e as QuickPickItemElement).item) + .map(e => e.item); + } + + setSelectedElements(items: IQuickPickItem[]) { + const elements = items.map(item => this._itemElements.find(e => e.item === item)) + .filter((e): e is QuickPickItemElement => !!e); + this._tree.setSelection(elements); + } + + getCheckedElements() { + return this._itemElements.filter(e => e.checked) + .map(e => e.item); + } + + setCheckedElements(items: IQuickPickItem[]) { + const checked = new Set(); + for (const item of items) { + checked.add(item); + } + for (const element of this._itemElements) { + element.checked = checked.has(element.item); + } + this._fireCheckedEvents(); + } + + focus(what: QuickInputListFocus): void { + if (!this._itemElements.length) { + return; + } + + if (what === QuickInputListFocus.Second && this._itemElements.length < 2) { + what = QuickInputListFocus.First; + } + + switch (what) { + case QuickInputListFocus.First: + this._tree.scrollTop = 0; + this._tree.focusFirst(undefined, (e) => e.element instanceof QuickPickItemElement); + break; + case QuickInputListFocus.Second: + this._tree.scrollTop = 0; + this._tree.setFocus([this._itemElements[1]]); + break; + case QuickInputListFocus.Last: + this._tree.scrollTop = this._tree.scrollHeight; + this._tree.setFocus([this._itemElements[this._itemElements.length - 1]]); + break; + case QuickInputListFocus.Next: + this._tree.focusNext(undefined, true, undefined, (e) => { + if (!(e.element instanceof QuickPickItemElement)) { + return false; + } + this._tree.reveal(e.element); + return true; + }); + break; + case QuickInputListFocus.Previous: + this._tree.focusPrevious(undefined, true, undefined, (e) => { + if (!(e.element instanceof QuickPickItemElement)) { + return false; + } + const parent = this._tree.getParentElement(e.element); + if (parent === null || (parent as QuickPickSeparatorElement).children[0] !== e.element) { + this._tree.reveal(e.element); + } else { + // Only if we are the first child of a separator do we reveal the separator + this._tree.reveal(parent); + } + return true; + }); + break; + case QuickInputListFocus.NextPage: + this._tree.focusNextPage(undefined, (e) => { + if (!(e.element instanceof QuickPickItemElement)) { + return false; + } + this._tree.reveal(e.element); + return true; + }); + break; + case QuickInputListFocus.PreviousPage: + this._tree.focusPreviousPage(undefined, (e) => { + if (!(e.element instanceof QuickPickItemElement)) { + return false; + } + const parent = this._tree.getParentElement(e.element); + if (parent === null || (parent as QuickPickSeparatorElement).children[0] !== e.element) { + this._tree.reveal(e.element); + } else { + this._tree.reveal(parent); + } + return true; + }); + break; + case QuickInputListFocus.NextSeparator: { + let foundSeparatorAsItem = false; + const before = this._tree.getFocus()[0]; + this._tree.focusNext(undefined, true, undefined, (e) => { + if (foundSeparatorAsItem) { + // This should be the index right after the separator so it + // is the item we want to focus. + return true; + } + + if (e.element instanceof QuickPickSeparatorElement) { + foundSeparatorAsItem = true; + // If the separator is visible, then we should just focus it. + if (this._separatorRenderer.isSeparatorVisible(e.element)) { + this._tree.reveal(e.element); + } else { + // If the separator is not visible, then we should + // push it up to the top of the list. + this._tree.reveal(e.element, 0); + } + } else if (e.element instanceof QuickPickItemElement) { + if (e.element.separator) { + if (this._itemRenderer.isItemWithSeparatorVisible(e.element)) { + this._tree.reveal(e.element); + } else { + this._tree.reveal(e.element, 0); + } + return true; + } else if (e.element === this._elementTree[0]) { + // We should stop at the first item in the list if it's a regular item. + this._tree.reveal(e.element, 0); + return true; + } + } + return false; + }); + const after = this._tree.getFocus()[0]; + if (before === after) { + // If we didn't move, then we should just move to the end + // of the list. + this._tree.scrollTop = this._tree.scrollHeight; + this._tree.setFocus([this._itemElements[this._itemElements.length - 1]]); + } + break; + } + case QuickInputListFocus.PreviousSeparator: { + let focusElement: IQuickPickElement | undefined; + // If we are already sitting on an inline separator, then we + // have already found the _current_ separator and need to + // move to the previous one. + let foundSeparator = !!this._tree.getFocus()[0]?.separator; + this._tree.focusPrevious(undefined, true, undefined, (e) => { + if (e.element instanceof QuickPickSeparatorElement) { + if (foundSeparator) { + if (!focusElement) { + if (this._separatorRenderer.isSeparatorVisible(e.element)) { + this._tree.reveal(e.element); + } else { + this._tree.reveal(e.element, 0); + } + focusElement = e.element.children[0]; + } + } else { + foundSeparator = true; + } + } else if (e.element instanceof QuickPickItemElement) { + if (!focusElement) { + if (e.element.separator) { + if (this._itemRenderer.isItemWithSeparatorVisible(e.element)) { + this._tree.reveal(e.element); + } else { + this._tree.reveal(e.element, 0); + } + + focusElement = e.element; + } else if (e.element === this._elementTree[0]) { + // We should stop at the first item in the list if it's a regular item. + this._tree.reveal(e.element, 0); + return true; + } + } + } + return false; + }); + if (focusElement) { + this._tree.setFocus([focusElement]); + } + break; + } + } + } + + clearFocus() { + this._tree.setFocus([]); + } + + domFocus() { + this._tree.domFocus(); + } + + layout(maxHeight?: number): void { + this._tree.getHTMLElement().style.maxHeight = maxHeight ? `${ + // Make sure height aligns with list item heights + Math.floor(maxHeight / 44) * 44 + // Add some extra height so that it's clear there's more to scroll + + 6 + }px` : ''; + this._tree.layout(); + } + + filter(query: string): boolean { + if (!(this._sortByLabel || this._matchOnLabel || this._matchOnDescription || this._matchOnDetail)) { + this._tree.layout(); + return false; + } + + const queryWithWhitespace = query; + query = query.trim(); + + // Reset filtering + if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { + this._itemElements.forEach(element => { + element.labelHighlights = undefined; + element.descriptionHighlights = undefined; + element.detailHighlights = undefined; + element.hidden = false; + const previous = element.index && this._inputElements[element.index - 1]; + if (element.item) { + element.separator = previous && previous.type === 'separator' && !previous.buttons ? previous : undefined; + } + }); + } + + // Filter by value (since we support icons in labels, use $(..) aware fuzzy matching) + else { + let currentSeparator: IQuickPickSeparator | undefined; + this._elementTree.forEach(element => { + let labelHighlights: IMatch[] | undefined; + if (this.matchOnLabelMode === 'fuzzy') { + labelHighlights = this.matchOnLabel ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel)) ?? undefined : undefined; + } else { + labelHighlights = this.matchOnLabel ? matchesContiguousIconAware(queryWithWhitespace, parseLabelWithIcons(element.saneLabel)) ?? undefined : undefined; + } + const descriptionHighlights = this.matchOnDescription ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || '')) ?? undefined : undefined; + const detailHighlights = this.matchOnDetail ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDetail || '')) ?? undefined : undefined; + + if (labelHighlights || descriptionHighlights || detailHighlights) { + element.labelHighlights = labelHighlights; + element.descriptionHighlights = descriptionHighlights; + element.detailHighlights = detailHighlights; + element.hidden = false; + } else { + element.labelHighlights = undefined; + element.descriptionHighlights = undefined; + element.detailHighlights = undefined; + element.hidden = element.item ? !element.item.alwaysShow : true; + } + + // Ensure separators are filtered out first before deciding if we need to bring them back + if (element.item) { + element.separator = undefined; + } else if (element.separator) { + element.hidden = true; + } + + // we can show the separator unless the list gets sorted by match + if (!this.sortByLabel) { + const previous = element.index && this._inputElements[element.index - 1]; + currentSeparator = previous && previous.type === 'separator' ? previous : currentSeparator; + if (currentSeparator && !element.hidden) { + element.separator = currentSeparator; + currentSeparator = undefined; + } + } + }); + } + + const shownElements = this._elementTree.filter(element => !element.hidden); + + // Sort by value + if (this.sortByLabel && query) { + const normalizedSearchValue = query.toLowerCase(); + shownElements.sort((a, b) => { + return compareEntries(a, b, normalizedSearchValue); + }); + } + + let currentSeparator: QuickPickSeparatorElement | undefined; + const finalElements = shownElements.reduce((result, element, index) => { + if (element instanceof QuickPickItemElement) { + if (currentSeparator) { + currentSeparator.children.push(element); + } else { + result.push(element); + } + } else if (element instanceof QuickPickSeparatorElement) { + element.children = []; + currentSeparator = element; + result.push(element); + } + return result; + }, new Array()); + + const elements = new Array>(); + for (const element of finalElements) { + if (element instanceof QuickPickSeparatorElement) { + elements.push({ + element, + collapsible: false, + collapsed: false, + children: element.children.map(e => ({ + element: e, + collapsible: false, + collapsed: false, + })), + }); + } else { + elements.push({ + element, + collapsible: false, + collapsed: false, + }); + } + } + this._tree.setChildren(null, elements); + this._tree.layout(); + + this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); + this._onChangedVisibleCount.fire(shownElements.length); + + return true; + } + + toggleCheckbox() { + const elements = this._tree.getFocus().filter((e): e is IQuickPickElement => !!e); + const allChecked = this._allVisibleChecked(elements); + for (const element of elements) { + element.checked = !allChecked; + } + this._fireCheckedEvents(); + } + + display(display: boolean) { + this._container.style.display = display ? '' : 'none'; + } + + isDisplayed() { + return this._container.style.display !== 'none'; + } + + style(styles: IListStyles) { + this._tree.style(styles); + } + + toggleHover() { + const focused: IQuickPickElement | null = this._tree.getFocus()[0]; + if (!focused?.saneTooltip || !(focused instanceof QuickPickItemElement)) { + return; + } + + // if there's a hover already, hide it (toggle off) + if (this._lastHover && !this._lastHover.isDisposed) { + this._lastHover.dispose(); + return; + } + + // If there is no hover, show it (toggle on) + this.showHover(focused); + const store = new DisposableStore(); + store.add(this._tree.onDidChangeFocus(e => { + if (e.elements[0] instanceof QuickPickItemElement) { + this.showHover(e.elements[0]); + } + })); + if (this._lastHover) { + store.add(this._lastHover); + } + this._elementDisposable.add(store); + } + + //#endregion + + //#region private methods + + private _allVisibleChecked(elements: IQuickPickElement[], whenNoneVisible = true) { + for (let i = 0, n = elements.length; i < n; i++) { + const element = elements[i]; + if (!element.hidden) { + if (!element.checked) { + return false; + } else { + whenNoneVisible = true; + } + } + } + return whenNoneVisible; + } + + private _fireCheckedEvents() { + this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); + this._onChangedCheckedCount.fire(this.getCheckedCount()); + this._onChangedCheckedElements.fire(this.getCheckedElements()); + } + + private fireButtonTriggered(event: IQuickPickItemButtonEvent) { + this._onButtonTriggered.fire(event); + } + + private fireSeparatorButtonTriggered(event: IQuickPickSeparatorButtonEvent) { + this._onSeparatorButtonTriggered.fire(event); + } + + /** + * Disposes of the hover and shows a new one for the given index if it has a tooltip. + * @param element The element to show the hover for + */ + private showHover(element: QuickPickItemElement): void { + if (this._lastHover && !this._lastHover.isDisposed) { + this.hoverDelegate.onDidHideHover?.(); + this._lastHover?.dispose(); + } + + if (!element.element || !element.saneTooltip) { + return; + } + this._lastHover = this.hoverDelegate.showHover({ + content: element.saneTooltip, + target: element.element, + linkHandler: (url) => { + this.linkOpenerDelegate(url); + }, + appearance: { + showPointer: true, + }, + container: this._container, + position: { + hoverPosition: HoverPosition.RIGHT + } + }, false); + } +} + +function matchesContiguousIconAware(query: string, target: IParsedLabelWithIcons): IMatch[] | null { + + const { text, iconOffsets } = target; + + // Return early if there are no icon markers in the word to match against + if (!iconOffsets || iconOffsets.length === 0) { + return matchesContiguous(query, text); + } + + // Trim the word to match against because it could have leading + // whitespace now if the word started with an icon + const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' '); + const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length; + + // match on value without icon + const matches = matchesContiguous(query, wordToMatchAgainstWithoutIconsTrimmed); + + // Map matches back to offsets with icon and trimming + if (matches) { + for (const match of matches) { + const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */; + match.start += iconOffset; + match.end += iconOffset; + } + } + + return matches; +} + +function matchesContiguous(word: string, wordToMatchAgainst: string): IMatch[] | null { + const matchIndex = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase()); + if (matchIndex !== -1) { + return [{ start: matchIndex, end: matchIndex + word.length }]; + } + return null; +} + +function compareEntries(elementA: IQuickPickElement, elementB: IQuickPickElement, lookFor: string): number { + + const labelHighlightsA = elementA.labelHighlights || []; + const labelHighlightsB = elementB.labelHighlights || []; + if (labelHighlightsA.length && !labelHighlightsB.length) { + return -1; + } + + if (!labelHighlightsA.length && labelHighlightsB.length) { + return 1; + } + + if (labelHighlightsA.length === 0 && labelHighlightsB.length === 0) { + return 0; + } + + return compareAnything(elementA.saneSortLabel, elementB.saneSortLabel, lookFor); +} diff --git a/code/src/vs/platform/quickinput/common/quickAccess.ts b/code/src/vs/platform/quickinput/common/quickAccess.ts index 47dc660daca..c160bb1fb93 100644 --- a/code/src/vs/platform/quickinput/common/quickAccess.ts +++ b/code/src/vs/platform/quickinput/common/quickAccess.ts @@ -6,7 +6,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ItemActivation, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { ItemActivation, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; /** @@ -22,6 +22,11 @@ export interface IQuickAccessProviderRunOptions { */ export interface AnythingQuickAccessProviderRunOptions extends IQuickAccessProviderRunOptions { readonly includeHelp?: boolean; + /** + * @deprecated - temporary for Dynamic Chat Variables (see usage) until it has built-in UX for file picking + * Useful for adding items to the top of the list that might contain actions. + */ + readonly additionPicks?: QuickPickItem[]; } export interface IQuickAccessOptions { diff --git a/code/src/vs/platform/quickinput/common/quickInput.ts b/code/src/vs/platform/quickinput/common/quickInput.ts index 4ccce2c7120..ef907680e5c 100644 --- a/code/src/vs/platform/quickinput/common/quickInput.ts +++ b/code/src/vs/platform/quickinput/common/quickInput.ts @@ -209,6 +209,11 @@ export interface IQuickInput extends IDisposable { */ readonly onDidHide: Event; + /** + * An event that is fired when the quick input will be hidden. + */ + readonly onWillHide: Event; + /** * An event that is fired when the quick input is disposed. */ @@ -285,6 +290,12 @@ export interface IQuickInput extends IDisposable { * @param reason The reason why the quick input was hidden. */ didHide(reason?: QuickInputHideReason): void; + + /** + * Notifies that the quick input will be hidden. + * @param reason The reason why the quick input will be hidden. + */ + willHide(reason?: QuickInputHideReason): void; } export interface IQuickWidget extends IQuickInput { diff --git a/code/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/code/src/vs/platform/quickinput/test/browser/quickinput.test.ts index f2a71af5553..216aa3a7f54 100644 --- a/code/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/code/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import { unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListOptions, List, unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; import { unthemedToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; +import { Event } from 'vs/base/common/event'; 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'; @@ -19,7 +19,19 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { toDisposable } from 'vs/base/common/lifecycle'; import { mainWindow } from 'vs/base/browser/window'; import { QuickPick } from 'vs/platform/quickinput/browser/quickInput'; -import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickItem, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IListService, ListService } from 'vs/platform/list/browser/listService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; +import { NoMatchingKb } from 'vs/platform/keybinding/common/keybindingResolver'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; // Sets up an `onShow` listener to allow us to wait until the quick pick is shown (useful when triggering an `accept()` right after launching a quick pick) // kick this off before you launch the picker and then await the promise returned after you launch the picker. @@ -45,50 +57,58 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 mainWindow.document.body.appendChild(fixture); store.add(toDisposable(() => mainWindow.document.body.removeChild(fixture))); - controller = store.add(new QuickInputController({ - container: fixture, - idPrefix: 'testQuickInput', - ignoreFocusOut() { return true; }, - returnFocus() { }, - backKeybindingLabel() { return undefined; }, - setContextKey() { return undefined; }, - linkOpenerDelegate(content) { }, - createList: ( - user: string, - container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], - options: IListOptions, - ) => new List(user, container, delegate, renderers, options), - hoverDelegate: { - showHover(options, focus) { - return undefined; - }, - delay: 200 - }, - styles: { - button: unthemedButtonStyles, - countBadge: unthemedCountStyles, - inputBox: unthemedInboxStyles, - toggle: unthemedToggleStyles, - keybindingLabel: unthemedKeybindingLabelOptions, - list: unthemedListStyles, - progressBar: unthemedProgressBarOptions, - widget: { - quickInputBackground: undefined, - quickInputForeground: undefined, - quickInputTitleBackground: undefined, - widgetBorder: undefined, - widgetShadow: undefined, + const instantiationService = new TestInstantiationService(); + + // Stub the services the quick input controller needs to function + instantiationService.stub(IThemeService, new TestThemeService()); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IListService, store.add(new ListService())); + instantiationService.stub(ILayoutService, { activeContainer: fixture, onDidLayoutContainer: Event.None } as any); + instantiationService.stub(IContextViewService, store.add(instantiationService.createInstance(ContextViewService))); + instantiationService.stub(IContextKeyService, store.add(instantiationService.createInstance(ContextKeyService))); + instantiationService.stub(IKeybindingService, { + mightProducePrintableCharacter() { return false; }, + softDispatch() { return NoMatchingKb; }, + }); + + controller = store.add(instantiationService.createInstance( + QuickInputController, + { + container: fixture, + idPrefix: 'testQuickInput', + ignoreFocusOut() { return true; }, + returnFocus() { }, + backKeybindingLabel() { return undefined; }, + setContextKey() { return undefined; }, + linkOpenerDelegate(content) { }, + hoverDelegate: { + showHover(options, focus) { + return undefined; + }, + delay: 200 }, - pickerGroup: { - pickerGroupBorder: undefined, - pickerGroupForeground: undefined, + styles: { + button: unthemedButtonStyles, + countBadge: unthemedCountStyles, + inputBox: unthemedInboxStyles, + toggle: unthemedToggleStyles, + keybindingLabel: unthemedKeybindingLabelOptions, + list: unthemedListStyles, + progressBar: unthemedProgressBarOptions, + widget: { + quickInputBackground: undefined, + quickInputForeground: undefined, + quickInputTitleBackground: undefined, + widgetBorder: undefined, + widgetShadow: undefined, + }, + pickerGroup: { + pickerGroupBorder: undefined, + pickerGroupForeground: undefined, + } } } - }, - new TestThemeService(), - { activeContainer: fixture } as any)); + )); // initial layout controller.layout({ height: 20, width: 40 }, 0); @@ -218,4 +238,41 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 // Since we don't select any items, the selected items should be empty assert.strictEqual(quickpick.selectedItems.length, 0); }); + + test('activeItems - verify onDidChangeActive is triggered after setting items', async () => { + const quickpick = store.add(controller.createQuickPick()); + + // Setup listener for verification + const activeItemsFromEvent: IQuickPickItem[] = []; + store.add(quickpick.onDidChangeActive(items => activeItemsFromEvent.push(...items))); + + quickpick.show(); + + const item = { label: 'step 1' }; + quickpick.items = [item]; + + assert.strictEqual(activeItemsFromEvent.length, 1); + assert.strictEqual(activeItemsFromEvent[0], item); + assert.strictEqual(quickpick.activeItems.length, 1); + assert.strictEqual(quickpick.activeItems[0], item); + }); + + test('activeItems - verify setting itemActivation to None still triggers onDidChangeActive after selection #207832', async () => { + const quickpick = store.add(controller.createQuickPick()); + const item = { label: 'step 1' }; + quickpick.items = [item]; + quickpick.show(); + assert.strictEqual(quickpick.activeItems[0], item); + + // Setup listener for verification + const activeItemsFromEvent: IQuickPickItem[] = []; + store.add(quickpick.onDidChangeActive(items => activeItemsFromEvent.push(...items))); + + // Trigger a change + quickpick.itemActivation = ItemActivation.NONE; + quickpick.items = [item]; + + assert.strictEqual(activeItemsFromEvent.length, 0); + assert.strictEqual(quickpick.activeItems.length, 0); + }); }); diff --git a/code/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/code/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 66defd005d1..087f5858b48 100644 --- a/code/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/code/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -6,7 +6,7 @@ import { IpcMainEvent, MessagePortMain } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, DeferredPromise } from 'vs/base/common/async'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -25,6 +25,7 @@ export class SharedProcess extends Disposable { private readonly firstWindowConnectionBarrier = new Barrier(); private utilityProcess: UtilityProcess | undefined = undefined; + private utilityProcessLogListener: IDisposable | undefined = undefined; constructor( private readonly machineId: string, @@ -104,13 +105,10 @@ export class SharedProcess extends Disposable { // all services within have been created. const whenReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); await whenReady.p; + this.utilityProcessLogListener?.dispose(); this.logService.trace('[SharedProcess] Overall ready'); })(); } @@ -131,11 +129,7 @@ export class SharedProcess extends Disposable { // Wait for shared process indicating that IPC connections are accepted const sharedProcessIpcReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); await sharedProcessIpcReady.p; this.logService.trace('[SharedProcess] IPC ready'); @@ -148,6 +142,15 @@ export class SharedProcess extends Disposable { private createUtilityProcess(): void { this.utilityProcess = this._register(new UtilityProcess(this.logService, NullTelemetryService, this.lifecycleMainService)); + // Install a log listener for very early shared process warnings and errors + this.utilityProcessLogListener = this.utilityProcess.onMessage((e: any) => { + if (typeof e.warning === 'string') { + this.logService.warn(e.warning); + } else if (typeof e.error === 'string') { + this.logService.error(e.error); + } + }); + const inspectParams = parseSharedProcessDebugPort(this.environmentMainService.args, this.environmentMainService.isBuilt); let execArgv: string[] | undefined = undefined; if (inspectParams.port) { diff --git a/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index df3cb4ce9ee..685bae340bb 100644 --- a/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -515,8 +515,6 @@ class WindowsPtyHeuristics extends Disposable { private _onCursorMoveListener = this._register(new MutableDisposable()); - private _recentlyPerformedCsiJ = false; - private _tryAdjustCommandStartMarkerScheduler?: RunOnceScheduler; private _tryAdjustCommandStartMarkerScannedLineCount: number = 0; private _tryAdjustCommandStartMarkerPollCount: number = 0; @@ -530,8 +528,8 @@ class WindowsPtyHeuristics extends Disposable { super(); this._register(_terminal.parser.registerCsiHandler({ final: 'J' }, params => { + // Clear commands when the viewport is cleared if (params.length >= 1 && (params[0] === 2 || params[0] === 3)) { - this._recentlyPerformedCsiJ = true; this._hooks.clearCommandsInViewport(); } // We don't want to override xterm.js' default behavior, just augment it @@ -539,11 +537,6 @@ class WindowsPtyHeuristics extends Disposable { })); this._register(this._capability.onBeforeCommandFinished(command => { - if (this._recentlyPerformedCsiJ) { - this._recentlyPerformedCsiJ = false; - return; - } - // For older Windows backends we cannot listen to CSI J, instead we assume running clear // or cls will clear all commands in the viewport. This is not perfect but it's right // most of the time. diff --git a/code/src/vs/platform/terminal/common/terminal.ts b/code/src/vs/platform/terminal/common/terminal.ts index 8a51aaa3ae1..3a27c6d7a0c 100644 --- a/code/src/vs/platform/terminal/common/terminal.ts +++ b/code/src/vs/platform/terminal/common/terminal.ts @@ -103,6 +103,7 @@ export const enum TerminalSettingId { PersistentSessionReviveProcess = 'terminal.integrated.persistentSessionReviveProcess', HideOnStartup = 'terminal.integrated.hideOnStartup', CustomGlyphs = 'terminal.integrated.customGlyphs', + RescaleOverlappingGlyphs = 'terminal.integrated.rescaleOverlappingGlyphs', PersistentSessionScrollback = 'terminal.integrated.persistentSessionScrollback', InheritEnv = 'terminal.integrated.inheritEnv', ShowLinkHover = 'terminal.integrated.showLinkHover', @@ -122,6 +123,7 @@ export const enum TerminalSettingId { StickyScrollEnabled = 'terminal.integrated.stickyScroll.enabled', StickyScrollMaxLineCount = 'terminal.integrated.stickyScroll.maxLineCount', MouseWheelZoom = 'terminal.integrated.mouseWheelZoom', + ExperimentalInlineChat = 'terminal.integrated.experimentalInlineChat', // Debug settings that are hidden from user @@ -568,7 +570,7 @@ export interface IShellLaunchConfig { * until `Terminal.show` is called. The typical usage for this is when you need to run * something that may need interactivity but only want to tell the user about it when * interaction is needed. Note that the terminals will still be exposed to all extensions - * as normal and they will remain hidden when the workspace is reloaded. + * as normal. The hidden terminals will not be restored when the workspace is next opened. */ hideFromUser?: boolean; diff --git a/code/src/vs/platform/terminal/common/terminalEnvironment.ts b/code/src/vs/platform/terminal/common/terminalEnvironment.ts index 38e8fa2c669..5ddf2aa71d6 100644 --- a/code/src/vs/platform/terminal/common/terminalEnvironment.ts +++ b/code/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { OperatingSystem, OS } from 'vs/base/common/platform'; +import type { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; /** * Aggressively escape non-windows paths to prepare for being sent to a shell. This will do some @@ -59,3 +60,11 @@ export function sanitizeCwd(cwd: string): string { } return cwd; } + +/** + * Determines whether the given shell launch config should use the environment variable collection. + * @param slc The shell launch config to check. + */ +export function shouldUseEnvironmentVariableCollection(slc: IShellLaunchConfig): boolean { + return !slc.strictEnv; +} diff --git a/code/src/vs/platform/terminal/node/terminalEnvironment.ts b/code/src/vs/platform/terminal/node/terminalEnvironment.ts index 703e0fec623..c59f1ddefd9 100644 --- a/code/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/code/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -112,12 +112,23 @@ export function getShellIntegrationInjection( logService: ILogService, productService: IProductService ): IShellIntegrationConfigInjection | undefined { - // Shell integration arg injection is disabled when: + // Conditionally disable shell integration arg injection // - The global setting is disabled // - There is no executable (not sure what script to run) // - The terminal is used by a feature like tasks or debugging const useWinpty = isWindows && (!options.windowsEnableConpty || getWindowsBuildNumber() < 18309); - if (!options.shellIntegration.enabled || !shellLaunchConfig.executable || (shellLaunchConfig.isFeatureTerminal && !shellLaunchConfig.forceShellIntegration) || shellLaunchConfig.hideFromUser || shellLaunchConfig.ignoreShellIntegration || useWinpty) { + if ( + // The global setting is disabled + !options.shellIntegration.enabled || + // There is no executable (so there's no way to determine how to inject) + !shellLaunchConfig.executable || + // It's a feature terminal (tasks, debug), unless it's explicitly being forced + (shellLaunchConfig.isFeatureTerminal && !shellLaunchConfig.forceShellIntegration) || + // The ignoreShellIntegration flag is passed (eg. relaunching without shell integration) + shellLaunchConfig.ignoreShellIntegration || + // Winpty is unsupported + useWinpty + ) { return undefined; } diff --git a/code/src/vs/platform/theme/common/colorRegistry.ts b/code/src/vs/platform/theme/common/colorRegistry.ts index c0948533749..82b65f7a795 100644 --- a/code/src/vs/platform/theme/common/colorRegistry.ts +++ b/code/src/vs/platform/theme/common/colorRegistry.ts @@ -3,700 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertNever } from 'vs/base/common/assert'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { Color, RGBA } from 'vs/base/common/color'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; -import * as nls from 'vs/nls'; -import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; -import * as platform from 'vs/platform/registry/common/platform'; -import { IColorTheme } from 'vs/platform/theme/common/themeService'; - -// ------ API types - -export type ColorIdentifier = string; - -export interface ColorContribution { - readonly id: ColorIdentifier; - readonly description: string; - readonly defaults: ColorDefaults | null; - readonly needsTransparency: boolean; - readonly deprecationMessage: string | undefined; -} - -/** - * Returns the css variable name for the given color identifier. Dots (`.`) are replaced with hyphens (`-`) and - * everything is prefixed with `--vscode-`. - * - * @sample `editorSuggestWidget.background` is `--vscode-editorSuggestWidget-background`. - */ -export function asCssVariableName(colorIdent: ColorIdentifier): string { - return `--vscode-${colorIdent.replace(/\./g, '-')}`; -} - -export function asCssVariable(color: ColorIdentifier): string { - return `var(${asCssVariableName(color)})`; -} - -export function asCssVariableWithDefault(color: ColorIdentifier, defaultCssValue: string): string { - return `var(${asCssVariableName(color)}, ${defaultCssValue})`; -} - -export const enum ColorTransformType { - Darken, - Lighten, - Transparent, - Opaque, - OneOf, - LessProminent, - IfDefinedThenElse -} - -export type ColorTransform = - | { op: ColorTransformType.Darken; value: ColorValue; factor: number } - | { op: ColorTransformType.Lighten; value: ColorValue; factor: number } - | { op: ColorTransformType.Transparent; value: ColorValue; factor: number } - | { op: ColorTransformType.Opaque; value: ColorValue; background: ColorValue } - | { op: ColorTransformType.OneOf; values: readonly ColorValue[] } - | { op: ColorTransformType.LessProminent; value: ColorValue; background: ColorValue; factor: number; transparency: number } - | { op: ColorTransformType.IfDefinedThenElse; if: ColorIdentifier; then: ColorValue; else: ColorValue }; - -export interface ColorDefaults { - light: ColorValue | null; - dark: ColorValue | null; - hcDark: ColorValue | null; - hcLight: ColorValue | null; -} - - -/** - * A Color Value is either a color literal, a reference to an other color or a derived color - */ -export type ColorValue = Color | string | ColorIdentifier | ColorTransform; - -// color registry -export const Extensions = { - ColorContribution: 'base.contributions.colors' -}; - -export interface IColorRegistry { - - readonly onDidChangeSchema: Event; - - /** - * Register a color to the registry. - * @param id The color id as used in theme description files - * @param defaults The default values - * @param needsTransparency Whether the color requires transparency - * @description the description - */ - registerColor(id: string, defaults: ColorDefaults, description: string, needsTransparency?: boolean): ColorIdentifier; - - /** - * Register a color to the registry. - */ - deregisterColor(id: string): void; - - /** - * Get all color contributions - */ - getColors(): ColorContribution[]; - - /** - * Gets the default color of the given id - */ - resolveDefaultColor(id: ColorIdentifier, theme: IColorTheme): Color | undefined; - - /** - * JSON schema for an object to assign color values to one of the color contributions. - */ - getColorSchema(): IJSONSchema; - - /** - * JSON schema to for a reference to a color contribution. - */ - getColorReferenceSchema(): IJSONSchema; - -} - -class ColorRegistry implements IColorRegistry { - - private readonly _onDidChangeSchema = new Emitter(); - readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; - - private colorsById: { [key: string]: ColorContribution }; - private colorSchema: IJSONSchema & { properties: IJSONSchemaMap } = { type: 'object', properties: {} }; - private colorReferenceSchema: IJSONSchema & { enum: string[]; enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; - - constructor() { - this.colorsById = {}; - } - - public registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { - const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; - this.colorsById[id] = colorContribution; - const propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; - if (deprecationMessage) { - propertySchema.deprecationMessage = deprecationMessage; - } - if (needsTransparency) { - propertySchema.pattern = '^#(?:(?[0-9a-fA-f]{3}[0-9a-eA-E])|(?:[0-9a-fA-F]{6}(?:(?![fF]{2})(?:[0-9a-fA-F]{2}))))?$'; - propertySchema.patternErrorMessage = 'This color must be transparent or it will obscure content'; - } - this.colorSchema.properties[id] = propertySchema; - this.colorReferenceSchema.enum.push(id); - this.colorReferenceSchema.enumDescriptions.push(description); - - this._onDidChangeSchema.fire(); - return id; - } - - - public deregisterColor(id: string): void { - delete this.colorsById[id]; - delete this.colorSchema.properties[id]; - const index = this.colorReferenceSchema.enum.indexOf(id); - if (index !== -1) { - this.colorReferenceSchema.enum.splice(index, 1); - this.colorReferenceSchema.enumDescriptions.splice(index, 1); - } - this._onDidChangeSchema.fire(); - } - - public getColors(): ColorContribution[] { - return Object.keys(this.colorsById).map(id => this.colorsById[id]); - } - - public resolveDefaultColor(id: ColorIdentifier, theme: IColorTheme): Color | undefined { - const colorDesc = this.colorsById[id]; - if (colorDesc && colorDesc.defaults) { - const colorValue = colorDesc.defaults[theme.type]; - return resolveColorValue(colorValue, theme); - } - return undefined; - } - - public getColorSchema(): IJSONSchema { - return this.colorSchema; - } - - public getColorReferenceSchema(): IJSONSchema { - return this.colorReferenceSchema; - } - - public toString() { - const sorter = (a: string, b: string) => { - const cat1 = a.indexOf('.') === -1 ? 0 : 1; - const cat2 = b.indexOf('.') === -1 ? 0 : 1; - if (cat1 !== cat2) { - return cat1 - cat2; - } - return a.localeCompare(b); - }; - - return Object.keys(this.colorsById).sort(sorter).map(k => `- \`${k}\`: ${this.colorsById[k].description}`).join('\n'); - } - -} - -const colorRegistry = new ColorRegistry(); -platform.Registry.add(Extensions.ColorContribution, colorRegistry); - - -export function registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { - return colorRegistry.registerColor(id, defaults, description, needsTransparency, deprecationMessage); -} - -export function getColorRegistry(): IColorRegistry { - return colorRegistry; -} - -// ----- base colors - -export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#616161', hcDark: '#FFFFFF', hcLight: '#292929' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component.")); -export const disabledForeground = registerColor('disabledForeground', { dark: '#CCCCCC80', light: '#61616180', hcDark: '#A5A5A5', hcLight: '#7F7F7F' }, nls.localize('disabledForeground', "Overall foreground for disabled elements. This color is only used if not overridden by a component.")); -export const errorForeground = registerColor('errorForeground', { dark: '#F48771', light: '#A1260D', hcDark: '#F48771', hcLight: '#B5200D' }, nls.localize('errorForeground', "Overall foreground color for error messages. This color is only used if not overridden by a component.")); -export const descriptionForeground = registerColor('descriptionForeground', { light: '#717171', dark: transparent(foreground, 0.7), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label.")); -export const iconForeground = registerColor('icon.foreground', { dark: '#C5C5C5', light: '#424242', hcDark: '#FFFFFF', hcLight: '#292929' }, nls.localize('iconForeground', "The default color for icons in the workbench.")); - -export const focusBorder = registerColor('focusBorder', { dark: '#007FD4', light: '#0090F1', hcDark: '#F38518', hcLight: '#006BBD' }, nls.localize('focusBorder', "Overall border color for focused elements. This color is only used if not overridden by a component.")); - -export const contrastBorder = registerColor('contrastBorder', { light: null, dark: null, hcDark: '#6FC3DF', hcLight: '#0F4A85' }, nls.localize('contrastBorder', "An extra border around elements to separate them from others for greater contrast.")); -export const activeContrastBorder = registerColor('contrastActiveBorder', { light: null, dark: null, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); - -export const selectionBackground = registerColor('selection.background', { light: null, dark: null, hcDark: null, hcLight: null }, nls.localize('selectionBackground', "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.")); - -// ------ text colors - -export const textSeparatorForeground = registerColor('textSeparator.foreground', { light: '#0000002e', dark: '#ffffff2e', hcDark: Color.black, hcLight: '#292929' }, nls.localize('textSeparatorForeground', "Color for text separators.")); - -export const textLinkForeground = registerColor('textLink.foreground', { light: '#006AB1', dark: '#3794FF', hcDark: '#21A6FF', hcLight: '#0F4A85' }, nls.localize('textLinkForeground', "Foreground color for links in text.")); -export const textLinkActiveForeground = registerColor('textLink.activeForeground', { light: '#006AB1', dark: '#3794FF', hcDark: '#21A6FF', hcLight: '#0F4A85' }, nls.localize('textLinkActiveForeground', "Foreground color for links in text when clicked on and on mouse hover.")); - -export const textPreformatForeground = registerColor('textPreformat.foreground', { light: '#A31515', dark: '#D7BA7D', hcDark: '#000000', hcLight: '#FFFFFF' }, nls.localize('textPreformatForeground', "Foreground color for preformatted text segments.")); -export const textPreformatBackground = registerColor('textPreformat.background', { light: '#0000001A', dark: '#FFFFFF1A', hcDark: '#FFFFFF', hcLight: '#09345f' }, nls.localize('textPreformatBackground', "Background color for preformatted text segments.")); -export const textBlockQuoteBackground = registerColor('textBlockQuote.background', { light: '#f2f2f2', dark: '#222222', hcDark: null, hcLight: '#F2F2F2' }, nls.localize('textBlockQuoteBackground', "Background color for block quotes in text.")); -export const textBlockQuoteBorder = registerColor('textBlockQuote.border', { light: '#007acc80', dark: '#007acc80', hcDark: Color.white, hcLight: '#292929' }, nls.localize('textBlockQuoteBorder', "Border color for block quotes in text.")); -export const textCodeBlockBackground = registerColor('textCodeBlock.background', { light: '#dcdcdc66', dark: '#0a0a0a66', hcDark: Color.black, hcLight: '#F2F2F2' }, nls.localize('textCodeBlockBackground', "Background color for code blocks in text.")); - -// ----- widgets -export const widgetShadow = registerColor('widget.shadow', { dark: transparent(Color.black, .36), light: transparent(Color.black, .16), hcDark: null, hcLight: null }, nls.localize('widgetShadow', 'Shadow color of widgets such as find/replace inside the editor.')); -export const widgetBorder = registerColor('widget.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('widgetBorder', 'Border color of widgets such as find/replace inside the editor.')); - -export const inputBackground = registerColor('input.background', { dark: '#3C3C3C', light: Color.white, hcDark: Color.black, hcLight: Color.white }, nls.localize('inputBoxBackground', "Input box background.")); -export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('inputBoxForeground', "Input box foreground.")); -export const inputBorder = registerColor('input.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputBoxBorder', "Input box border.")); -export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC', light: '#007ACC', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields.")); -export const inputActiveOptionHoverBackground = registerColor('inputOption.hoverBackground', { dark: '#5a5d5e80', light: '#b8b8b850', hcDark: null, hcLight: null }, nls.localize('inputOption.hoverBackground', "Background color of activated options in input fields.")); -export const inputActiveOptionBackground = registerColor('inputOption.activeBackground', { dark: transparent(focusBorder, 0.4), light: transparent(focusBorder, 0.2), hcDark: Color.transparent, hcLight: Color.transparent }, nls.localize('inputOption.activeBackground', "Background hover color of options in input fields.")); -export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', { dark: Color.white, light: Color.black, hcDark: foreground, hcLight: foreground }, nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields.")); -export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); - -export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hcDark: Color.black, hcLight: Color.white }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); -export const inputValidationInfoForeground = registerColor('inputValidation.infoForeground', { dark: null, light: null, hcDark: null, hcLight: foreground }, nls.localize('inputValidationInfoForeground', "Input validation foreground color for information severity.")); -export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity.")); -export const inputValidationWarningBackground = registerColor('inputValidation.warningBackground', { dark: '#352A05', light: '#F6F5D2', hcDark: Color.black, hcLight: Color.white }, nls.localize('inputValidationWarningBackground', "Input validation background color for warning severity.")); -export const inputValidationWarningForeground = registerColor('inputValidation.warningForeground', { dark: null, light: null, hcDark: null, hcLight: foreground }, nls.localize('inputValidationWarningForeground', "Input validation foreground color for warning severity.")); -export const inputValidationWarningBorder = registerColor('inputValidation.warningBorder', { dark: '#B89500', light: '#B89500', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputValidationWarningBorder', "Input validation border color for warning severity.")); -export const inputValidationErrorBackground = registerColor('inputValidation.errorBackground', { dark: '#5A1D1D', light: '#F2DEDE', hcDark: Color.black, hcLight: Color.white }, nls.localize('inputValidationErrorBackground', "Input validation background color for error severity.")); -export const inputValidationErrorForeground = registerColor('inputValidation.errorForeground', { dark: null, light: null, hcDark: null, hcLight: foreground }, nls.localize('inputValidationErrorForeground', "Input validation foreground color for error severity.")); -export const inputValidationErrorBorder = registerColor('inputValidation.errorBorder', { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputValidationErrorBorder', "Input validation border color for error severity.")); - -export const selectBackground = registerColor('dropdown.background', { dark: '#3C3C3C', light: Color.white, hcDark: Color.black, hcLight: Color.white }, nls.localize('dropdownBackground', "Dropdown background.")); -export const selectListBackground = registerColor('dropdown.listBackground', { dark: null, light: null, hcDark: Color.black, hcLight: Color.white }, nls.localize('dropdownListBackground', "Dropdown list background.")); -export const selectForeground = registerColor('dropdown.foreground', { dark: '#F0F0F0', light: foreground, hcDark: Color.white, hcLight: foreground }, nls.localize('dropdownForeground', "Dropdown foreground.")); -export const selectBorder = registerColor('dropdown.border', { dark: selectBackground, light: '#CECECE', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('dropdownBorder', "Dropdown border.")); - -export const buttonForeground = registerColor('button.foreground', { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, nls.localize('buttonForeground', "Button foreground color.")); -export const buttonSeparator = registerColor('button.separator', { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: transparent(buttonForeground, .4), hcLight: transparent(buttonForeground, .4) }, nls.localize('buttonSeparator', "Button separator color.")); -export const buttonBackground = registerColor('button.background', { dark: '#0E639C', light: '#007ACC', hcDark: null, hcLight: '#0F4A85' }, nls.localize('buttonBackground', "Button background color.")); -export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hcDark: buttonBackground, hcLight: buttonBackground }, nls.localize('buttonHoverBackground', "Button background color when hovering.")); -export const buttonBorder = registerColor('button.border', { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('buttonBorder', "Button border color.")); - -export const buttonSecondaryForeground = registerColor('button.secondaryForeground', { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: foreground }, nls.localize('buttonSecondaryForeground', "Secondary button foreground color.")); -export const buttonSecondaryBackground = registerColor('button.secondaryBackground', { dark: '#3A3D41', light: '#5F6A79', hcDark: null, hcLight: Color.white }, nls.localize('buttonSecondaryBackground', "Secondary button background color.")); -export const buttonSecondaryHoverBackground = registerColor('button.secondaryHoverBackground', { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); - -export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#C4C4C4', hcDark: Color.black, hcLight: '#0F4A85' }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); -export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: '#333', hcDark: Color.white, hcLight: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count.")); - -export const scrollbarShadow = registerColor('scrollbar.shadow', { dark: '#000000', light: '#DDDDDD', hcDark: null, hcLight: null }, nls.localize('scrollbarShadow', "Scrollbar shadow to indicate that the view is scrolled.")); -export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hcDark: transparent(contrastBorder, 0.6), hcLight: transparent(contrastBorder, 0.4) }, nls.localize('scrollbarSliderBackground', "Scrollbar slider background color.")); -export const scrollbarSliderHoverBackground = registerColor('scrollbarSlider.hoverBackground', { dark: Color.fromHex('#646464').transparent(0.7), light: Color.fromHex('#646464').transparent(0.7), hcDark: transparent(contrastBorder, 0.8), hcLight: transparent(contrastBorder, 0.8) }, nls.localize('scrollbarSliderHoverBackground', "Scrollbar slider background color when hovering.")); -export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.activeBackground', { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('scrollbarSliderActiveBackground', "Scrollbar slider background color when clicked on.")); - -export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); - -export const editorErrorBackground = registerColor('editorError.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F14C4C', light: '#E51400', hcDark: '#F48771', hcLight: '#B5200D' }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); -export const editorErrorBorder = registerColor('editorError.border', { dark: null, light: null, hcDark: Color.fromHex('#E47777').transparent(0.8), hcLight: '#B5200D' }, nls.localize('errorBorder', 'If set, color of double underlines for errors in the editor.')); - -export const editorWarningBackground = registerColor('editorWarning.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorWarningForeground = registerColor('editorWarning.foreground', { dark: '#CCA700', light: '#BF8803', hcDark: '#FFD370', hcLight: '#895503' }, nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.')); -export const editorWarningBorder = registerColor('editorWarning.border', { dark: null, light: null, hcDark: Color.fromHex('#FFCC00').transparent(0.8), hcLight: Color.fromHex('#FFCC00').transparent(0.8) }, nls.localize('warningBorder', 'If set, color of double underlines for warnings in the editor.')); - -export const editorInfoBackground = registerColor('editorInfo.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorInfoForeground = registerColor('editorInfo.foreground', { dark: '#3794FF', light: '#1a85ff', hcDark: '#3794FF', hcLight: '#1a85ff' }, nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.')); -export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, light: null, hcDark: Color.fromHex('#3794FF').transparent(0.8), hcLight: '#292929' }, nls.localize('infoBorder', 'If set, color of double underlines for infos in the editor.')); - -export const editorHintForeground = registerColor('editorHint.foreground', { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hcDark: null, hcLight: null }, nls.localize('editorHint.foreground', 'Foreground color of hint squigglies in the editor.')); -export const editorHintBorder = registerColor('editorHint.border', { dark: null, light: null, hcDark: Color.fromHex('#eeeeee').transparent(0.8), hcLight: '#292929' }, nls.localize('hintBorder', 'If set, color of double underlines for hints in the editor.')); - -export const sashHoverBorder = registerColor('sash.hoverBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('sashActiveBorder', "Border color of active sashes.")); - -/** - * Editor background color. - */ -export const editorBackground = registerColor('editor.background', { light: '#ffffff', dark: '#1E1E1E', hcDark: Color.black, hcLight: Color.white }, nls.localize('editorBackground', "Editor background color.")); - -/** - * Editor foreground color. - */ -export const editorForeground = registerColor('editor.foreground', { light: '#333333', dark: '#BBBBBB', hcDark: Color.white, hcLight: foreground }, nls.localize('editorForeground', "Editor default foreground color.")); - -/** - * Sticky scroll - */ -export const editorStickyScrollBackground = registerColor('editorStickyScroll.background', { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, nls.localize('editorStickyScrollBackground', "Background color of sticky scroll in the editor")); -export const editorStickyScrollHoverBackground = registerColor('editorStickyScrollHover.background', { dark: '#2A2D2E', light: '#F0F0F0', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, nls.localize('editorStickyScrollHoverBackground', "Background color of sticky scroll on hover in the editor")); -export const editorStickyScrollBorder = registerColor('editorStickyScroll.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorStickyScrollBorder', "Border color of sticky scroll in the editor")); -export const editorStickyScrollShadow = registerColor('editorStickyScroll.shadow', { dark: scrollbarShadow, light: scrollbarShadow, hcDark: scrollbarShadow, hcLight: scrollbarShadow }, nls.localize('editorStickyScrollShadow', " Shadow color of sticky scroll in the editor")); - -/** - * Editor widgets - */ -export const editorWidgetBackground = registerColor('editorWidget.background', { dark: '#252526', light: '#F3F3F3', hcDark: '#0C141F', hcLight: Color.white }, nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); -export const editorWidgetForeground = registerColor('editorWidget.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); -export const editorWidgetBorder = registerColor('editorWidget.border', { dark: '#454545', light: '#C8C8C8', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); -export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', { light: null, dark: null, hcDark: null, hcLight: null }, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); - -/** - * Quick pick widget - */ -export const quickInputBackground = registerColor('quickInput.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); -export const quickInputForeground = registerColor('quickInput.foreground', { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); -export const quickInputTitleBackground = registerColor('quickInputTitle.background', { dark: new Color(new RGBA(255, 255, 255, 0.105)), light: new Color(new RGBA(0, 0, 0, 0.06)), hcDark: '#000000', hcLight: Color.white }, nls.localize('pickerTitleBackground', "Quick picker title background color. The quick picker widget is the container for pickers like the command palette.")); -export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: '#3794FF', light: '#0066BF', hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels.")); -export const pickerGroupBorder = registerColor('pickerGroup.border', { dark: '#3F3F46', light: '#CCCEDB', hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); - -/** - * Keybinding label - */ -export const keybindingLabelBackground = registerColor('keybindingLabel.background', { dark: new Color(new RGBA(128, 128, 128, 0.17)), light: new Color(new RGBA(221, 221, 221, 0.4)), hcDark: Color.transparent, hcLight: Color.transparent }, nls.localize('keybindingLabelBackground', "Keybinding label background color. The keybinding label is used to represent a keyboard shortcut.")); -export const keybindingLabelForeground = registerColor('keybindingLabel.foreground', { dark: Color.fromHex('#CCCCCC'), light: Color.fromHex('#555555'), hcDark: Color.white, hcLight: foreground }, nls.localize('keybindingLabelForeground', "Keybinding label foreground color. The keybinding label is used to represent a keyboard shortcut.")); -export const keybindingLabelBorder = registerColor('keybindingLabel.border', { dark: new Color(new RGBA(51, 51, 51, 0.6)), light: new Color(new RGBA(204, 204, 204, 0.4)), hcDark: new Color(new RGBA(111, 195, 223)), hcLight: contrastBorder }, nls.localize('keybindingLabelBorder', "Keybinding label border color. The keybinding label is used to represent a keyboard shortcut.")); -export const keybindingLabelBottomBorder = registerColor('keybindingLabel.bottomBorder', { dark: new Color(new RGBA(68, 68, 68, 0.6)), light: new Color(new RGBA(187, 187, 187, 0.4)), hcDark: new Color(new RGBA(111, 195, 223)), hcLight: foreground }, nls.localize('keybindingLabelBottomBorder', "Keybinding label border bottom color. The keybinding label is used to represent a keyboard shortcut.")); - -/** - * Editor selection colors. - */ -export const editorSelectionBackground = registerColor('editor.selectionBackground', { light: '#ADD6FF', dark: '#264F78', hcDark: '#f3f518', hcLight: '#0F4A85' }, nls.localize('editorSelectionBackground', "Color of the editor selection.")); -export const editorSelectionForeground = registerColor('editor.selectionForeground', { light: null, dark: null, hcDark: '#000000', hcLight: Color.white }, nls.localize('editorSelectionForeground', "Color of the selected text for high contrast.")); -export const editorInactiveSelection = registerColor('editor.inactiveSelectionBackground', { light: transparent(editorSelectionBackground, 0.5), dark: transparent(editorSelectionBackground, 0.5), hcDark: transparent(editorSelectionBackground, 0.7), hcLight: transparent(editorSelectionBackground, 0.5) }, nls.localize('editorInactiveSelection', "Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations."), true); -export const editorSelectionHighlight = registerColor('editor.selectionHighlightBackground', { light: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), dark: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), hcDark: null, hcLight: null }, nls.localize('editorSelectionHighlight', 'Color for regions with the same content as the selection. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorSelectionHighlightBorder = registerColor('editor.selectionHighlightBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('editorSelectionHighlightBorder', "Border color for regions with the same content as the selection.")); - - -/** - * Editor find match colors. - */ -export const editorFindMatch = registerColor('editor.findMatchBackground', { light: '#A8AC94', dark: '#515C6A', hcDark: null, hcLight: null }, nls.localize('editorFindMatch', "Color of the current search match.")); -export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', { light: '#EA5C0055', dark: '#EA5C0055', hcDark: null, hcLight: null }, nls.localize('findMatchHighlight', "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations."), true); -export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', { dark: '#3a3d4166', light: '#b4b4b44d', hcDark: null, hcLight: null }, nls.localize('findRangeHighlight', "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); -export const editorFindMatchBorder = registerColor('editor.findMatchBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('editorFindMatchBorder', "Border color of the current search match.")); -export const editorFindMatchHighlightBorder = registerColor('editor.findMatchHighlightBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('findMatchHighlightBorder', "Border color of the other search matches.")); -export const editorFindRangeHighlightBorder = registerColor('editor.findRangeHighlightBorder', { dark: null, light: null, hcDark: transparent(activeContrastBorder, 0.4), hcLight: transparent(activeContrastBorder, 0.4) }, nls.localize('findRangeHighlightBorder', "Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); - -/** - * Search Editor query match colors. - * - * Distinct from normal editor find match to allow for better differentiation - */ -export const searchEditorFindMatch = registerColor('searchEditor.findMatchBackground', { light: transparent(editorFindMatchHighlight, 0.66), dark: transparent(editorFindMatchHighlight, 0.66), hcDark: editorFindMatchHighlight, hcLight: editorFindMatchHighlight }, nls.localize('searchEditor.queryMatch', "Color of the Search Editor query matches.")); -export const searchEditorFindMatchBorder = registerColor('searchEditor.findMatchBorder', { light: transparent(editorFindMatchHighlightBorder, 0.66), dark: transparent(editorFindMatchHighlightBorder, 0.66), hcDark: editorFindMatchHighlightBorder, hcLight: editorFindMatchHighlightBorder }, nls.localize('searchEditor.editorFindMatchBorder', "Border color of the Search Editor query matches.")); - -/** - * Search Viewlet colors. - */ -export const searchResultsInfoForeground = registerColor('search.resultsInfoForeground', { light: foreground, dark: transparent(foreground, 0.65), hcDark: foreground, hcLight: foreground }, nls.localize('search.resultsInfoForeground', "Color of the text in the search viewlet's completion message.")); - -/** - * Editor hover - */ -export const editorHoverHighlight = registerColor('editor.hoverHighlightBackground', { light: '#ADD6FF26', dark: '#264f7840', hcDark: '#ADD6FF26', hcLight: null }, nls.localize('hoverHighlight', 'Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorHoverBackground = registerColor('editorHoverWidget.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('hoverBackground', 'Background color of the editor hover.')); -export const editorHoverForeground = registerColor('editorHoverWidget.foreground', { light: editorWidgetForeground, dark: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, nls.localize('hoverForeground', 'Foreground color of the editor hover.')); -export const editorHoverBorder = registerColor('editorHoverWidget.border', { light: editorWidgetBorder, dark: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, nls.localize('hoverBorder', 'Border color of the editor hover.')); -export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.statusBarBackground', { dark: lighten(editorHoverBackground, 0.2), light: darken(editorHoverBackground, 0.05), hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('statusBarBackground', "Background color of the editor hover status bar.")); -/** - * Editor link colors - */ -export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', { dark: '#4E94CE', light: Color.blue, hcDark: Color.cyan, hcLight: '#292929' }, nls.localize('activeLinkForeground', 'Color of active links.')); - -/** - * Inline hints - */ -export const editorInlayHintForeground = registerColor('editorInlayHint.foreground', { dark: '#969696', light: '#969696', hcDark: Color.white, hcLight: Color.black }, nls.localize('editorInlayHintForeground', 'Foreground color of inline hints')); -export const editorInlayHintBackground = registerColor('editorInlayHint.background', { dark: transparent(badgeBackground, .10), light: transparent(badgeBackground, .10), hcDark: transparent(Color.white, .10), hcLight: transparent(badgeBackground, .10) }, nls.localize('editorInlayHintBackground', 'Background color of inline hints')); -export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); -export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); -export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); -export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); - -/** - * Editor lightbulb icon colors - */ -export const editorLightBulbForeground = registerColor('editorLightBulb.foreground', { dark: '#FFCC00', light: '#DDB100', hcDark: '#FFCC00', hcLight: '#007ACC' }, nls.localize('editorLightBulbForeground', "The color used for the lightbulb actions icon.")); -export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAutoFix.foreground', { dark: '#75BEFF', light: '#007ACC', hcDark: '#75BEFF', hcLight: '#007ACC' }, nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); -export const editorLightBulbAiForeground = registerColor('editorLightBulbAi.foreground', { dark: editorLightBulbForeground, light: editorLightBulbForeground, hcDark: editorLightBulbForeground, hcLight: editorLightBulbForeground }, nls.localize('editorLightBulbAiForeground', "The color used for the lightbulb AI icon.")); - -/** - * Diff Editor Colors - */ -export const defaultInsertColor = new Color(new RGBA(155, 185, 85, .2)); -export const defaultRemoveColor = new Color(new RGBA(255, 0, 0, .2)); - -export const diffInserted = registerColor('diffEditor.insertedTextBackground', { dark: '#9ccc2c33', light: '#9ccc2c40', hcDark: null, hcLight: null }, nls.localize('diffEditorInserted', 'Background color for text that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); -export const diffRemoved = registerColor('diffEditor.removedTextBackground', { dark: '#ff000033', light: '#ff000033', hcDark: null, hcLight: null }, nls.localize('diffEditorRemoved', 'Background color for text that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); - -export const diffInsertedLine = registerColor('diffEditor.insertedLineBackground', { dark: defaultInsertColor, light: defaultInsertColor, hcDark: null, hcLight: null }, nls.localize('diffEditorInsertedLines', 'Background color for lines that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); -export const diffRemovedLine = registerColor('diffEditor.removedLineBackground', { dark: defaultRemoveColor, light: defaultRemoveColor, hcDark: null, hcLight: null }, nls.localize('diffEditorRemovedLines', 'Background color for lines that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); - -export const diffInsertedLineGutter = registerColor('diffEditorGutter.insertedLineBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorInsertedLineGutter', 'Background color for the margin where lines got inserted.')); -export const diffRemovedLineGutter = registerColor('diffEditorGutter.removedLineBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorRemovedLineGutter', 'Background color for the margin where lines got removed.')); - -export const diffOverviewRulerInserted = registerColor('diffEditorOverview.insertedForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorOverviewInserted', 'Diff overview ruler foreground for inserted content.')); -export const diffOverviewRulerRemoved = registerColor('diffEditorOverview.removedForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorOverviewRemoved', 'Diff overview ruler foreground for removed content.')); - -export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder', { dark: null, light: null, hcDark: '#33ff2eff', hcLight: '#374E06' }, nls.localize('diffEditorInsertedOutline', 'Outline color for the text that got inserted.')); -export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', { dark: null, light: null, hcDark: '#FF008F', hcLight: '#AD0707' }, nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.')); - -export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.')); -export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: '#cccccc33', light: '#22222233', hcDark: null, hcLight: null }, nls.localize('diffDiagonalFill', "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views.")); - -export const diffUnchangedRegionBackground = registerColor('diffEditor.unchangedRegionBackground', { dark: 'sideBar.background', light: 'sideBar.background', hcDark: 'sideBar.background', hcLight: 'sideBar.background' }, nls.localize('diffEditor.unchangedRegionBackground', "The background color of unchanged blocks in the diff editor.")); -export const diffUnchangedRegionForeground = registerColor('diffEditor.unchangedRegionForeground', { dark: 'foreground', light: 'foreground', hcDark: 'foreground', hcLight: 'foreground' }, nls.localize('diffEditor.unchangedRegionForeground', "The foreground color of unchanged blocks in the diff editor.")); -export const diffUnchangedTextBackground = registerColor('diffEditor.unchangedCodeBackground', { dark: '#74747429', light: '#b8b8b829', hcDark: null, hcLight: null }, nls.localize('diffEditor.unchangedCodeBackground', "The background color of unchanged code in the diff editor.")); - -/** - * List and tree colors - */ -export const listFocusBackground = registerColor('list.focusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listFocusOutline = registerColor('list.focusOutline', { dark: focusBorder, light: focusBorder, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listFocusAndSelectionOutline = registerColor('list.focusAndSelectionOutline', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listFocusAndSelectionOutline', "List/Tree outline color for the focused item when the list/tree is active and selected. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#04395E', light: '#0060C0', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hcDark: null, hcLight: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hcDark: Color.white.transparent(0.1), hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); -export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); -export const listDropOverBackground = registerColor('list.dropBackground', { dark: '#062F4A', light: '#D6EBFF', hcDark: null, hcLight: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items over other items when using the mouse.")); -export const listDropBetweenBackground = registerColor('list.dropBetweenBackground', { dark: iconForeground, light: iconForeground, hcDark: null, hcLight: null }, nls.localize('listDropBetweenBackground', "List/Tree drag and drop border color when moving items between items when using the mouse.")); -export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#2AAAFF', light: '#0066BF', hcDark: focusBorder, hcLight: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); -export const listFocusHighlightForeground = registerColor('list.focusHighlightForeground', { dark: listHighlightForeground, light: ifDefinedThenElse(listActiveSelectionBackground, listHighlightForeground, '#BBE7FF'), hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('listFocusHighlightForeground', 'List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.')); -export const listInvalidItemForeground = registerColor('list.invalidItemForeground', { dark: '#B89500', light: '#B89500', hcDark: '#B89500', hcLight: '#B5200D' }, nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.')); -export const listErrorForeground = registerColor('list.errorForeground', { dark: '#F88070', light: '#B01011', hcDark: null, hcLight: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); -export const listWarningForeground = registerColor('list.warningForeground', { dark: '#CCA700', light: '#855F00', hcDark: null, hcLight: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.')); -export const listFilterWidgetBackground = registerColor('listFilterWidget.background', { light: darken(editorWidgetBackground, 0), dark: lighten(editorWidgetBackground, 0), hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('listFilterWidgetBackground', 'Background color of the type filter widget in lists and trees.')); -export const listFilterWidgetOutline = registerColor('listFilterWidget.outline', { dark: Color.transparent, light: Color.transparent, hcDark: '#f38518', hcLight: '#007ACC' }, nls.localize('listFilterWidgetOutline', 'Outline color of the type filter widget in lists and trees.')); -export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget.noMatchesOutline', { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); -export const listFilterWidgetShadow = registerColor('listFilterWidget.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, nls.localize('listFilterWidgetShadow', 'Shadow color of the type filter widget in lists and trees.')); -export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hcDark: null, hcLight: null }, nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); -export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hcDark: contrastBorder, hcLight: activeContrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); -export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', { dark: '#585858', light: '#a9a9a9', hcDark: '#a9a9a9', hcLight: '#a5a5a5' }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); -export const treeInactiveIndentGuidesStroke = registerColor('tree.inactiveIndentGuidesStroke', { dark: transparent(treeIndentGuidesStroke, 0.4), light: transparent(treeIndentGuidesStroke, 0.4), hcDark: transparent(treeIndentGuidesStroke, 0.4), hcLight: transparent(treeIndentGuidesStroke, 0.4) }, nls.localize('treeInactiveIndentGuidesStroke', "Tree stroke color for the indentation guides that are not active.")); -export const tableColumnsBorder = registerColor('tree.tableColumnsBorder', { dark: '#CCCCCC20', light: '#61616120', hcDark: null, hcLight: null }, nls.localize('tableColumnsBorder', "Table border color between columns.")); -export const tableOddRowsBackgroundColor = registerColor('tree.tableOddRowsBackground', { dark: transparent(foreground, 0.04), light: transparent(foreground, 0.04), hcDark: null, hcLight: null }, nls.localize('tableOddRowsBackgroundColor', "Background color for odd table rows.")); -export const listDeemphasizedForeground = registerColor('list.deemphasizedForeground', { dark: '#8C8C8C', light: '#8E8E90', hcDark: '#A7A8A9', hcLight: '#666666' }, nls.localize('listDeemphasizedForeground', "List/Tree foreground color for items that are deemphasized. ")); - -/** - * Checkboxes - */ -export const checkboxBackground = registerColor('checkbox.background', { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, nls.localize('checkbox.background', "Background color of checkbox widget.")); -export const checkboxSelectBackground = registerColor('checkbox.selectBackground', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('checkbox.select.background', "Background color of checkbox widget when the element it's in is selected.")); -export const checkboxForeground = registerColor('checkbox.foreground', { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, nls.localize('checkbox.foreground', "Foreground color of checkbox widget.")); -export const checkboxBorder = registerColor('checkbox.border', { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, nls.localize('checkbox.border', "Border color of checkbox widget.")); -export const checkboxSelectBorder = registerColor('checkbox.selectBorder', { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, nls.localize('checkbox.select.border', "Border color of checkbox widget when the element it's in is selected.")); - -/** - * Quick pick widget (dependent on List and tree colors) - */ -export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); -export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); -export const quickInputListFocusIconForeground = registerColor('quickInputList.focusIconForeground', { dark: listActiveSelectionIconForeground, light: listActiveSelectionIconForeground, hcDark: listActiveSelectionIconForeground, hcLight: listActiveSelectionIconForeground }, nls.localize('quickInput.listFocusIconForeground', "Quick picker icon foreground color for the focused item.")); -export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', { dark: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), light: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), hcDark: null, hcLight: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); - -/** - * Menu colors - */ -export const menuBorder = registerColor('menu.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('menuBorder', "Border color of menus.")); -export const menuForeground = registerColor('menu.foreground', { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, nls.localize('menuForeground', "Foreground color of menu items.")); -export const menuBackground = registerColor('menu.background', { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, nls.localize('menuBackground', "Background color of menu items.")); -export const menuSelectionForeground = registerColor('menu.selectionForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); -export const menuSelectionBackground = registerColor('menu.selectionBackground', { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hcDark: listActiveSelectionBackground, hcLight: listActiveSelectionBackground }, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); -export const menuSelectionBorder = registerColor('menu.selectionBorder', { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); -export const menuSeparatorBackground = registerColor('menu.separatorBackground', { dark: '#606060', light: '#D4D4D4', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('menuSeparatorBackground', "Color of a separator menu item in menus.")); - -/** - * Toolbar colors - */ -export const toolbarHoverBackground = registerColor('toolbar.hoverBackground', { dark: '#5a5d5e50', light: '#b8b8b850', hcDark: null, hcLight: null }, nls.localize('toolbarHoverBackground', "Toolbar background when hovering over actions using the mouse")); -export const toolbarHoverOutline = registerColor('toolbar.hoverOutline', { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('toolbarHoverOutline', "Toolbar outline when hovering over actions using the mouse")); -export const toolbarActiveBackground = registerColor('toolbar.activeBackground', { dark: lighten(toolbarHoverBackground, 0.1), light: darken(toolbarHoverBackground, 0.1), hcDark: null, hcLight: null }, nls.localize('toolbarActiveBackground', "Toolbar background when holding the mouse over actions")); - -/** - * Snippet placeholder colors - */ -export const snippetTabstopHighlightBackground = registerColor('editor.snippetTabstopHighlightBackground', { dark: new Color(new RGBA(124, 124, 124, 0.3)), light: new Color(new RGBA(10, 50, 100, 0.2)), hcDark: new Color(new RGBA(124, 124, 124, 0.3)), hcLight: new Color(new RGBA(10, 50, 100, 0.2)) }, nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); -export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); -export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); -export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hcDark: '#525252', hcLight: '#292929' }, nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final tabstop of a snippet.")); - -/** - * Breadcrumb colors - */ -export const breadcrumbsForeground = registerColor('breadcrumb.foreground', { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hcDark: transparent(foreground, 0.8), hcLight: transparent(foreground, 0.8) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); -export const breadcrumbsBackground = registerColor('breadcrumb.background', { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); -export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); -export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, nls.localize('breadcrumbsSelectedForeground', "Color of selected breadcrumb items.")); -export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); - -/** - * Merge-conflict colors - */ - -const headerTransparency = 0.5; -const currentBaseColor = Color.fromHex('#40C8AE').transparent(headerTransparency); -const incomingBaseColor = Color.fromHex('#40A6FF').transparent(headerTransparency); -const commonBaseColor = Color.fromHex('#606060').transparent(0.4); -const contentTransparency = 0.4; -const rulerTransparency = 1; - -export const mergeCurrentHeaderBackground = registerColor('merge.currentHeaderBackground', { dark: currentBaseColor, light: currentBaseColor, hcDark: null, hcLight: null }, nls.localize('mergeCurrentHeaderBackground', 'Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeCurrentContentBackground = registerColor('merge.currentContentBackground', { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, nls.localize('mergeCurrentContentBackground', 'Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeaderBackground', { dark: incomingBaseColor, light: incomingBaseColor, hcDark: null, hcLight: null }, nls.localize('mergeIncomingHeaderBackground', 'Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeIncomingContentBackground = registerColor('merge.incomingContentBackground', { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, nls.localize('mergeIncomingContentBackground', 'Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBackground', { dark: commonBaseColor, light: commonBaseColor, hcDark: null, hcLight: null }, nls.localize('mergeCommonHeaderBackground', 'Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeCommonContentBackground = registerColor('merge.commonContentBackground', { dark: transparent(mergeCommonHeaderBackground, contentTransparency), light: transparent(mergeCommonHeaderBackground, contentTransparency), hcDark: transparent(mergeCommonHeaderBackground, contentTransparency), hcLight: transparent(mergeCommonHeaderBackground, contentTransparency) }, nls.localize('mergeCommonContentBackground', 'Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); - -export const mergeBorder = registerColor('merge.border', { dark: null, light: null, hcDark: '#C3DF6F', hcLight: '#007ACC' }, nls.localize('mergeBorder', 'Border color on headers and the splitter in inline merge-conflicts.')); - -export const overviewRulerCurrentContentForeground = registerColor('editorOverviewRuler.currentContentForeground', { dark: transparent(mergeCurrentHeaderBackground, rulerTransparency), light: transparent(mergeCurrentHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, nls.localize('overviewRulerCurrentContentForeground', 'Current overview ruler foreground for inline merge-conflicts.')); -export const overviewRulerIncomingContentForeground = registerColor('editorOverviewRuler.incomingContentForeground', { dark: transparent(mergeIncomingHeaderBackground, rulerTransparency), light: transparent(mergeIncomingHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, nls.localize('overviewRulerIncomingContentForeground', 'Incoming overview ruler foreground for inline merge-conflicts.')); -export const overviewRulerCommonContentForeground = registerColor('editorOverviewRuler.commonContentForeground', { dark: transparent(mergeCommonHeaderBackground, rulerTransparency), light: transparent(mergeCommonHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, nls.localize('overviewRulerCommonContentForeground', 'Common ancestor overview ruler foreground for inline merge-conflicts.')); - -export const overviewRulerFindMatchForeground = registerColor('editorOverviewRuler.findMatchForeground', { dark: '#d186167e', light: '#d186167e', hcDark: '#AB5A00', hcLight: '' }, nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); - -export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); - - -export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { light: '#d18616', dark: '#d18616', hcDark: '#AB5A00', hcLight: '#0F4A85' }, nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true); -export const minimapSelectionOccurrenceHighlight = registerColor('minimap.selectionOccurrenceHighlight', { light: '#c9c9c9', dark: '#676767', hcDark: '#ffffff', hcLight: '#0F4A85' }, nls.localize('minimapSelectionOccurrenceHighlight', 'Minimap marker color for repeating editor selections.'), true); -export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hcDark: '#ffffff', hcLight: '#0F4A85' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true); -export const minimapInfo = registerColor('minimap.infoHighlight', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoBorder, hcLight: editorInfoBorder }, nls.localize('minimapInfo', 'Minimap marker color for infos.')); -export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningBorder, hcLight: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); -export const minimapError = registerColor('minimap.errorHighlight', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '#B5200D' }, nls.localize('minimapError', 'Minimap marker color for errors.')); -export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('minimapBackground', "Minimap background color.")); -export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hcDark: Color.fromHex('#000f'), hcLight: Color.fromHex('#000f') }, nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); - -export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hcDark: transparent(scrollbarSliderBackground, 0.5), hcLight: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color.")); -export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hcDark: transparent(scrollbarSliderHoverBackground, 0.5), hcLight: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); -export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hcDark: transparent(scrollbarSliderActiveBackground, 0.5), hcLight: transparent(scrollbarSliderActiveBackground, 0.5) }, nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); - -export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); -export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); -export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); - -/** - * Chart colors - */ -export const chartsForeground = registerColor('charts.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('chartsForeground', "The foreground color used in charts.")); -export const chartsLines = registerColor('charts.lines', { dark: transparent(foreground, .5), light: transparent(foreground, .5), hcDark: transparent(foreground, .5), hcLight: transparent(foreground, .5) }, nls.localize('chartsLines', "The color used for horizontal lines in charts.")); -export const chartsRed = registerColor('charts.red', { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, nls.localize('chartsRed', "The red color used in chart visualizations.")); -export const chartsBlue = registerColor('charts.blue', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, nls.localize('chartsBlue', "The blue color used in chart visualizations.")); -export const chartsYellow = registerColor('charts.yellow', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); -export const chartsOrange = registerColor('charts.orange', { dark: minimapFindMatch, light: minimapFindMatch, hcDark: minimapFindMatch, hcLight: minimapFindMatch }, nls.localize('chartsOrange', "The orange color used in chart visualizations.")); -export const chartsGreen = registerColor('charts.green', { dark: '#89D185', light: '#388A34', hcDark: '#89D185', hcLight: '#374e06' }, nls.localize('chartsGreen', "The green color used in chart visualizations.")); -export const chartsPurple = registerColor('charts.purple', { dark: '#B180D7', light: '#652D90', hcDark: '#B180D7', hcLight: '#652D90' }, nls.localize('chartsPurple', "The purple color used in chart visualizations.")); - -// ----- color functions - -export function executeTransform(transform: ColorTransform, theme: IColorTheme): Color | undefined { - switch (transform.op) { - case ColorTransformType.Darken: - return resolveColorValue(transform.value, theme)?.darken(transform.factor); - - case ColorTransformType.Lighten: - return resolveColorValue(transform.value, theme)?.lighten(transform.factor); - - case ColorTransformType.Transparent: - return resolveColorValue(transform.value, theme)?.transparent(transform.factor); - - case ColorTransformType.Opaque: { - const backgroundColor = resolveColorValue(transform.background, theme); - if (!backgroundColor) { - return resolveColorValue(transform.value, theme); - } - return resolveColorValue(transform.value, theme)?.makeOpaque(backgroundColor); - } - - case ColorTransformType.OneOf: - for (const candidate of transform.values) { - const color = resolveColorValue(candidate, theme); - if (color) { - return color; - } - } - return undefined; - - case ColorTransformType.IfDefinedThenElse: - return resolveColorValue(theme.defines(transform.if) ? transform.then : transform.else, theme); - - case ColorTransformType.LessProminent: { - const from = resolveColorValue(transform.value, theme); - if (!from) { - return undefined; - } - - const backgroundColor = resolveColorValue(transform.background, theme); - if (!backgroundColor) { - return from.transparent(transform.factor * transform.transparency); - } - - return from.isDarkerThan(backgroundColor) - ? Color.getLighterColor(from, backgroundColor, transform.factor).transparent(transform.transparency) - : Color.getDarkerColor(from, backgroundColor, transform.factor).transparent(transform.transparency); - } - default: - throw assertNever(transform); - } -} - -export function darken(colorValue: ColorValue, factor: number): ColorTransform { - return { op: ColorTransformType.Darken, value: colorValue, factor }; -} - -export function lighten(colorValue: ColorValue, factor: number): ColorTransform { - return { op: ColorTransformType.Lighten, value: colorValue, factor }; -} - -export function transparent(colorValue: ColorValue, factor: number): ColorTransform { - return { op: ColorTransformType.Transparent, value: colorValue, factor }; -} - -export function opaque(colorValue: ColorValue, background: ColorValue): ColorTransform { - return { op: ColorTransformType.Opaque, value: colorValue, background }; -} - -export function oneOf(...colorValues: ColorValue[]): ColorTransform { - return { op: ColorTransformType.OneOf, values: colorValues }; -} - -export function ifDefinedThenElse(ifArg: ColorIdentifier, thenArg: ColorValue, elseArg: ColorValue): ColorTransform { - return { op: ColorTransformType.IfDefinedThenElse, if: ifArg, then: thenArg, else: elseArg }; -} - -function lessProminent(colorValue: ColorValue, backgroundColorValue: ColorValue, factor: number, transparency: number): ColorTransform { - return { op: ColorTransformType.LessProminent, value: colorValue, background: backgroundColorValue, factor, transparency }; -} - -// ----- implementation - -/** - * @param colorValue Resolve a color value in the context of a theme - */ -export function resolveColorValue(colorValue: ColorValue | null, theme: IColorTheme): Color | undefined { - if (colorValue === null) { - return undefined; - } else if (typeof colorValue === 'string') { - if (colorValue[0] === '#') { - return Color.fromHex(colorValue); - } - return theme.getColor(colorValue); - } else if (colorValue instanceof Color) { - return colorValue; - } else if (typeof colorValue === 'object') { - return executeTransform(colorValue, theme); - } - return undefined; -} - -export const workbenchColorsSchemaId = 'vscode://schemas/workbench-colors'; - -const schemaRegistry = platform.Registry.as(JSONExtensions.JSONContribution); -schemaRegistry.registerSchema(workbenchColorsSchemaId, colorRegistry.getColorSchema()); - -const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(workbenchColorsSchemaId), 200); -colorRegistry.onDidChangeSchema(() => { - if (!delayer.isScheduled()) { - delayer.schedule(); - } -}); - -// setTimeout(_ => console.log(colorRegistry.toString()), 5000); +export * from 'vs/platform/theme/common/colorUtils'; + +// Make sure all color files are exported +export * from 'vs/platform/theme/common/colors/baseColors'; +export * from 'vs/platform/theme/common/colors/chartsColors'; +export * from 'vs/platform/theme/common/colors/editorColors'; +export * from 'vs/platform/theme/common/colors/inputColors'; +export * from 'vs/platform/theme/common/colors/listColors'; +export * from 'vs/platform/theme/common/colors/menuColors'; +export * from 'vs/platform/theme/common/colors/minimapColors'; +export * from 'vs/platform/theme/common/colors/miscColors'; +export * from 'vs/platform/theme/common/colors/quickpickColors'; +export * from 'vs/platform/theme/common/colors/searchColors'; diff --git a/code/src/vs/platform/theme/common/colorUtils.ts b/code/src/vs/platform/theme/common/colorUtils.ts new file mode 100644 index 00000000000..2388e7cb702 --- /dev/null +++ b/code/src/vs/platform/theme/common/colorUtils.ts @@ -0,0 +1,328 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertNever } from 'vs/base/common/assert'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Color } from 'vs/base/common/color'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import * as platform from 'vs/platform/registry/common/platform'; +import { IColorTheme } from 'vs/platform/theme/common/themeService'; + +// ------ API types + +export type ColorIdentifier = string; + +export interface ColorContribution { + readonly id: ColorIdentifier; + readonly description: string; + readonly defaults: ColorDefaults | null; + readonly needsTransparency: boolean; + readonly deprecationMessage: string | undefined; +} + +/** + * Returns the css variable name for the given color identifier. Dots (`.`) are replaced with hyphens (`-`) and + * everything is prefixed with `--vscode-`. + * + * @sample `editorSuggestWidget.background` is `--vscode-editorSuggestWidget-background`. + */ +export function asCssVariableName(colorIdent: ColorIdentifier): string { + return `--vscode-${colorIdent.replace(/\./g, '-')}`; +} + +export function asCssVariable(color: ColorIdentifier): string { + return `var(${asCssVariableName(color)})`; +} + +export function asCssVariableWithDefault(color: ColorIdentifier, defaultCssValue: string): string { + return `var(${asCssVariableName(color)}, ${defaultCssValue})`; +} + +export const enum ColorTransformType { + Darken, + Lighten, + Transparent, + Opaque, + OneOf, + LessProminent, + IfDefinedThenElse +} + +export type ColorTransform = + | { op: ColorTransformType.Darken; value: ColorValue; factor: number } + | { op: ColorTransformType.Lighten; value: ColorValue; factor: number } + | { op: ColorTransformType.Transparent; value: ColorValue; factor: number } + | { op: ColorTransformType.Opaque; value: ColorValue; background: ColorValue } + | { op: ColorTransformType.OneOf; values: readonly ColorValue[] } + | { op: ColorTransformType.LessProminent; value: ColorValue; background: ColorValue; factor: number; transparency: number } + | { op: ColorTransformType.IfDefinedThenElse; if: ColorIdentifier; then: ColorValue; else: ColorValue }; + +export interface ColorDefaults { + light: ColorValue | null; + dark: ColorValue | null; + hcDark: ColorValue | null; + hcLight: ColorValue | null; +} + + +/** + * A Color Value is either a color literal, a reference to an other color or a derived color + */ +export type ColorValue = Color | string | ColorIdentifier | ColorTransform; + +// color registry +export const Extensions = { + ColorContribution: 'base.contributions.colors' +}; + +export interface IColorRegistry { + + readonly onDidChangeSchema: Event; + + /** + * Register a color to the registry. + * @param id The color id as used in theme description files + * @param defaults The default values + * @param needsTransparency Whether the color requires transparency + * @description the description + */ + registerColor(id: string, defaults: ColorDefaults, description: string, needsTransparency?: boolean): ColorIdentifier; + + /** + * Register a color to the registry. + */ + deregisterColor(id: string): void; + + /** + * Get all color contributions + */ + getColors(): ColorContribution[]; + + /** + * Gets the default color of the given id + */ + resolveDefaultColor(id: ColorIdentifier, theme: IColorTheme): Color | undefined; + + /** + * JSON schema for an object to assign color values to one of the color contributions. + */ + getColorSchema(): IJSONSchema; + + /** + * JSON schema to for a reference to a color contribution. + */ + getColorReferenceSchema(): IJSONSchema; + +} + +class ColorRegistry implements IColorRegistry { + + private readonly _onDidChangeSchema = new Emitter(); + readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; + + private colorsById: { [key: string]: ColorContribution }; + private colorSchema: IJSONSchema & { properties: IJSONSchemaMap } = { type: 'object', properties: {} }; + private colorReferenceSchema: IJSONSchema & { enum: string[]; enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; + + constructor() { + this.colorsById = {}; + } + + public registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { + const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; + this.colorsById[id] = colorContribution; + const propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; + if (deprecationMessage) { + propertySchema.deprecationMessage = deprecationMessage; + } + if (needsTransparency) { + propertySchema.pattern = '^#(?:(?[0-9a-fA-f]{3}[0-9a-eA-E])|(?:[0-9a-fA-F]{6}(?:(?![fF]{2})(?:[0-9a-fA-F]{2}))))?$'; + propertySchema.patternErrorMessage = 'This color must be transparent or it will obscure content'; + } + this.colorSchema.properties[id] = propertySchema; + this.colorReferenceSchema.enum.push(id); + this.colorReferenceSchema.enumDescriptions.push(description); + + this._onDidChangeSchema.fire(); + return id; + } + + + public deregisterColor(id: string): void { + delete this.colorsById[id]; + delete this.colorSchema.properties[id]; + const index = this.colorReferenceSchema.enum.indexOf(id); + if (index !== -1) { + this.colorReferenceSchema.enum.splice(index, 1); + this.colorReferenceSchema.enumDescriptions.splice(index, 1); + } + this._onDidChangeSchema.fire(); + } + + public getColors(): ColorContribution[] { + return Object.keys(this.colorsById).map(id => this.colorsById[id]); + } + + public resolveDefaultColor(id: ColorIdentifier, theme: IColorTheme): Color | undefined { + const colorDesc = this.colorsById[id]; + if (colorDesc && colorDesc.defaults) { + const colorValue = colorDesc.defaults[theme.type]; + return resolveColorValue(colorValue, theme); + } + return undefined; + } + + public getColorSchema(): IJSONSchema { + return this.colorSchema; + } + + public getColorReferenceSchema(): IJSONSchema { + return this.colorReferenceSchema; + } + + public toString() { + const sorter = (a: string, b: string) => { + const cat1 = a.indexOf('.') === -1 ? 0 : 1; + const cat2 = b.indexOf('.') === -1 ? 0 : 1; + if (cat1 !== cat2) { + return cat1 - cat2; + } + return a.localeCompare(b); + }; + + return Object.keys(this.colorsById).sort(sorter).map(k => `- \`${k}\`: ${this.colorsById[k].description}`).join('\n'); + } + +} + +const colorRegistry = new ColorRegistry(); +platform.Registry.add(Extensions.ColorContribution, colorRegistry); + + +export function registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { + return colorRegistry.registerColor(id, defaults, description, needsTransparency, deprecationMessage); +} + +export function getColorRegistry(): IColorRegistry { + return colorRegistry; +} + +// ----- color functions + +export function executeTransform(transform: ColorTransform, theme: IColorTheme): Color | undefined { + switch (transform.op) { + case ColorTransformType.Darken: + return resolveColorValue(transform.value, theme)?.darken(transform.factor); + + case ColorTransformType.Lighten: + return resolveColorValue(transform.value, theme)?.lighten(transform.factor); + + case ColorTransformType.Transparent: + return resolveColorValue(transform.value, theme)?.transparent(transform.factor); + + case ColorTransformType.Opaque: { + const backgroundColor = resolveColorValue(transform.background, theme); + if (!backgroundColor) { + return resolveColorValue(transform.value, theme); + } + return resolveColorValue(transform.value, theme)?.makeOpaque(backgroundColor); + } + + case ColorTransformType.OneOf: + for (const candidate of transform.values) { + const color = resolveColorValue(candidate, theme); + if (color) { + return color; + } + } + return undefined; + + case ColorTransformType.IfDefinedThenElse: + return resolveColorValue(theme.defines(transform.if) ? transform.then : transform.else, theme); + + case ColorTransformType.LessProminent: { + const from = resolveColorValue(transform.value, theme); + if (!from) { + return undefined; + } + + const backgroundColor = resolveColorValue(transform.background, theme); + if (!backgroundColor) { + return from.transparent(transform.factor * transform.transparency); + } + + return from.isDarkerThan(backgroundColor) + ? Color.getLighterColor(from, backgroundColor, transform.factor).transparent(transform.transparency) + : Color.getDarkerColor(from, backgroundColor, transform.factor).transparent(transform.transparency); + } + default: + throw assertNever(transform); + } +} + +export function darken(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Darken, value: colorValue, factor }; +} + +export function lighten(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Lighten, value: colorValue, factor }; +} + +export function transparent(colorValue: ColorValue, factor: number): ColorTransform { + return { op: ColorTransformType.Transparent, value: colorValue, factor }; +} + +export function opaque(colorValue: ColorValue, background: ColorValue): ColorTransform { + return { op: ColorTransformType.Opaque, value: colorValue, background }; +} + +export function oneOf(...colorValues: ColorValue[]): ColorTransform { + return { op: ColorTransformType.OneOf, values: colorValues }; +} + +export function ifDefinedThenElse(ifArg: ColorIdentifier, thenArg: ColorValue, elseArg: ColorValue): ColorTransform { + return { op: ColorTransformType.IfDefinedThenElse, if: ifArg, then: thenArg, else: elseArg }; +} + +export function lessProminent(colorValue: ColorValue, backgroundColorValue: ColorValue, factor: number, transparency: number): ColorTransform { + return { op: ColorTransformType.LessProminent, value: colorValue, background: backgroundColorValue, factor, transparency }; +} + +// ----- implementation + +/** + * @param colorValue Resolve a color value in the context of a theme + */ +export function resolveColorValue(colorValue: ColorValue | null, theme: IColorTheme): Color | undefined { + if (colorValue === null) { + return undefined; + } else if (typeof colorValue === 'string') { + if (colorValue[0] === '#') { + return Color.fromHex(colorValue); + } + return theme.getColor(colorValue); + } else if (colorValue instanceof Color) { + return colorValue; + } else if (typeof colorValue === 'object') { + return executeTransform(colorValue, theme); + } + return undefined; +} + +export const workbenchColorsSchemaId = 'vscode://schemas/workbench-colors'; + +const schemaRegistry = platform.Registry.as(JSONExtensions.JSONContribution); +schemaRegistry.registerSchema(workbenchColorsSchemaId, colorRegistry.getColorSchema()); + +const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(workbenchColorsSchemaId), 200); +colorRegistry.onDidChangeSchema(() => { + if (!delayer.isScheduled()) { + delayer.schedule(); + } +}); + +// setTimeout(_ => console.log(colorRegistry.toString()), 5000); diff --git a/code/src/vs/platform/theme/common/colors/baseColors.ts b/code/src/vs/platform/theme/common/colors/baseColors.ts new file mode 100644 index 00000000000..1d19b3adc1f --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/baseColors.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { Color } from 'vs/base/common/color'; +import { registerColor, transparent } from 'vs/platform/theme/common/colorUtils'; + + +export const foreground = registerColor('foreground', + { dark: '#CCCCCC', light: '#616161', hcDark: '#FFFFFF', hcLight: '#292929' }, + nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component.")); + +export const disabledForeground = registerColor('disabledForeground', + { dark: '#CCCCCC80', light: '#61616180', hcDark: '#A5A5A5', hcLight: '#7F7F7F' }, + nls.localize('disabledForeground', "Overall foreground for disabled elements. This color is only used if not overridden by a component.")); + +export const errorForeground = registerColor('errorForeground', + { dark: '#F48771', light: '#A1260D', hcDark: '#F48771', hcLight: '#B5200D' }, + nls.localize('errorForeground', "Overall foreground color for error messages. This color is only used if not overridden by a component.")); + +export const descriptionForeground = registerColor('descriptionForeground', + { light: '#717171', dark: transparent(foreground, 0.7), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, + nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label.")); + +export const iconForeground = registerColor('icon.foreground', + { dark: '#C5C5C5', light: '#424242', hcDark: '#FFFFFF', hcLight: '#292929' }, + nls.localize('iconForeground', "The default color for icons in the workbench.")); + +export const focusBorder = registerColor('focusBorder', + { dark: '#007FD4', light: '#0090F1', hcDark: '#F38518', hcLight: '#006BBD' }, + nls.localize('focusBorder', "Overall border color for focused elements. This color is only used if not overridden by a component.")); + +export const contrastBorder = registerColor('contrastBorder', + { light: null, dark: null, hcDark: '#6FC3DF', hcLight: '#0F4A85' }, + nls.localize('contrastBorder', "An extra border around elements to separate them from others for greater contrast.")); + +export const activeContrastBorder = registerColor('contrastActiveBorder', + { light: null, dark: null, hcDark: focusBorder, hcLight: focusBorder }, + nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); + +export const selectionBackground = registerColor('selection.background', + { light: null, dark: null, hcDark: null, hcLight: null }, + nls.localize('selectionBackground', "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.")); + + +// ------ text link + +export const textLinkForeground = registerColor('textLink.foreground', + { light: '#006AB1', dark: '#3794FF', hcDark: '#21A6FF', hcLight: '#0F4A85' }, + nls.localize('textLinkForeground', "Foreground color for links in text.")); + +export const textLinkActiveForeground = registerColor('textLink.activeForeground', + { light: '#006AB1', dark: '#3794FF', hcDark: '#21A6FF', hcLight: '#0F4A85' }, + nls.localize('textLinkActiveForeground', "Foreground color for links in text when clicked on and on mouse hover.")); + +export const textSeparatorForeground = registerColor('textSeparator.foreground', + { light: '#0000002e', dark: '#ffffff2e', hcDark: Color.black, hcLight: '#292929' }, + nls.localize('textSeparatorForeground', "Color for text separators.")); + + +// ------ text preformat + +export const textPreformatForeground = registerColor('textPreformat.foreground', + { light: '#A31515', dark: '#D7BA7D', hcDark: '#000000', hcLight: '#FFFFFF' }, + nls.localize('textPreformatForeground', "Foreground color for preformatted text segments.")); + +export const textPreformatBackground = registerColor('textPreformat.background', + { light: '#0000001A', dark: '#FFFFFF1A', hcDark: '#FFFFFF', hcLight: '#09345f' }, + nls.localize('textPreformatBackground', "Background color for preformatted text segments.")); + + +// ------ text block quote + +export const textBlockQuoteBackground = registerColor('textBlockQuote.background', + { light: '#f2f2f2', dark: '#222222', hcDark: null, hcLight: '#F2F2F2' }, + nls.localize('textBlockQuoteBackground', "Background color for block quotes in text.")); + +export const textBlockQuoteBorder = registerColor('textBlockQuote.border', + { light: '#007acc80', dark: '#007acc80', hcDark: Color.white, hcLight: '#292929' }, + nls.localize('textBlockQuoteBorder', "Border color for block quotes in text.")); + + +// ------ text code block + +export const textCodeBlockBackground = registerColor('textCodeBlock.background', + { light: '#dcdcdc66', dark: '#0a0a0a66', hcDark: Color.black, hcLight: '#F2F2F2' }, + nls.localize('textCodeBlockBackground', "Background color for code blocks in text.")); diff --git a/code/src/vs/platform/theme/common/colors/chartsColors.ts b/code/src/vs/platform/theme/common/colors/chartsColors.ts new file mode 100644 index 00000000000..eb63b602234 --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/chartsColors.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 * as nls from 'vs/nls'; +import { registerColor, transparent } from 'vs/platform/theme/common/colorUtils'; + +import { foreground } from 'vs/platform/theme/common/colors/baseColors'; +import { editorErrorForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colors/editorColors'; +import { minimapFindMatch } from 'vs/platform/theme/common/colors/minimapColors'; + + +export const chartsForeground = registerColor('charts.foreground', + { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + nls.localize('chartsForeground', "The foreground color used in charts.")); + +export const chartsLines = registerColor('charts.lines', + { dark: transparent(foreground, .5), light: transparent(foreground, .5), hcDark: transparent(foreground, .5), hcLight: transparent(foreground, .5) }, + nls.localize('chartsLines', "The color used for horizontal lines in charts.")); + +export const chartsRed = registerColor('charts.red', + { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + nls.localize('chartsRed', "The red color used in chart visualizations.")); + +export const chartsBlue = registerColor('charts.blue', + { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + nls.localize('chartsBlue', "The blue color used in chart visualizations.")); + +export const chartsYellow = registerColor('charts.yellow', + { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); + +export const chartsOrange = registerColor('charts.orange', + { dark: minimapFindMatch, light: minimapFindMatch, hcDark: minimapFindMatch, hcLight: minimapFindMatch }, + nls.localize('chartsOrange', "The orange color used in chart visualizations.")); + +export const chartsGreen = registerColor('charts.green', + { dark: '#89D185', light: '#388A34', hcDark: '#89D185', hcLight: '#374e06' }, + nls.localize('chartsGreen', "The green color used in chart visualizations.")); + +export const chartsPurple = registerColor('charts.purple', + { dark: '#B180D7', light: '#652D90', hcDark: '#B180D7', hcLight: '#652D90' }, + nls.localize('chartsPurple', "The purple color used in chart visualizations.")); diff --git a/code/src/vs/platform/theme/common/colors/editorColors.ts b/code/src/vs/platform/theme/common/colors/editorColors.ts new file mode 100644 index 00000000000..4116f5ec141 --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/editorColors.ts @@ -0,0 +1,441 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { Color, RGBA } from 'vs/base/common/color'; +import { registerColor, transparent, lessProminent, darken, lighten } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { foreground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colors/baseColors'; +import { scrollbarShadow, badgeBackground } from 'vs/platform/theme/common/colors/miscColors'; + + +// ----- editor + +export const editorBackground = registerColor('editor.background', + { light: '#ffffff', dark: '#1E1E1E', hcDark: Color.black, hcLight: Color.white }, + nls.localize('editorBackground', "Editor background color.")); + +export const editorForeground = registerColor('editor.foreground', + { light: '#333333', dark: '#BBBBBB', hcDark: Color.white, hcLight: foreground }, + nls.localize('editorForeground', "Editor default foreground color.")); + + +export const editorStickyScrollBackground = registerColor('editorStickyScroll.background', + { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + nls.localize('editorStickyScrollBackground', "Background color of sticky scroll in the editor")); + +export const editorStickyScrollHoverBackground = registerColor('editorStickyScrollHover.background', + { dark: '#2A2D2E', light: '#F0F0F0', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, + nls.localize('editorStickyScrollHoverBackground', "Background color of sticky scroll on hover in the editor")); + +export const editorStickyScrollBorder = registerColor('editorStickyScroll.border', + { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('editorStickyScrollBorder', "Border color of sticky scroll in the editor")); + +export const editorStickyScrollShadow = registerColor('editorStickyScroll.shadow', + { dark: scrollbarShadow, light: scrollbarShadow, hcDark: scrollbarShadow, hcLight: scrollbarShadow }, + nls.localize('editorStickyScrollShadow', " Shadow color of sticky scroll in the editor")); + + +export const editorWidgetBackground = registerColor('editorWidget.background', + { dark: '#252526', light: '#F3F3F3', hcDark: '#0C141F', hcLight: Color.white }, + nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); + +export const editorWidgetForeground = registerColor('editorWidget.foreground', + { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); + +export const editorWidgetBorder = registerColor('editorWidget.border', + { dark: '#454545', light: '#C8C8C8', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); + +export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', + { light: null, dark: null, hcDark: null, hcLight: null }, + nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); + + +export const editorErrorBackground = registerColor('editorError.background', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const editorErrorForeground = registerColor('editorError.foreground', + { dark: '#F14C4C', light: '#E51400', hcDark: '#F48771', hcLight: '#B5200D' }, + nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); + +export const editorErrorBorder = registerColor('editorError.border', + { dark: null, light: null, hcDark: Color.fromHex('#E47777').transparent(0.8), hcLight: '#B5200D' }, + nls.localize('errorBorder', 'If set, color of double underlines for errors in the editor.')); + + +export const editorWarningBackground = registerColor('editorWarning.background', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const editorWarningForeground = registerColor('editorWarning.foreground', + { dark: '#CCA700', light: '#BF8803', hcDark: '#FFD370', hcLight: '#895503' }, + nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.')); + +export const editorWarningBorder = registerColor('editorWarning.border', + { dark: null, light: null, hcDark: Color.fromHex('#FFCC00').transparent(0.8), hcLight: Color.fromHex('#FFCC00').transparent(0.8) }, + nls.localize('warningBorder', 'If set, color of double underlines for warnings in the editor.')); + + +export const editorInfoBackground = registerColor('editorInfo.background', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const editorInfoForeground = registerColor('editorInfo.foreground', + { dark: '#3794FF', light: '#1a85ff', hcDark: '#3794FF', hcLight: '#1a85ff' }, + nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.')); + +export const editorInfoBorder = registerColor('editorInfo.border', + { dark: null, light: null, hcDark: Color.fromHex('#3794FF').transparent(0.8), hcLight: '#292929' }, + nls.localize('infoBorder', 'If set, color of double underlines for infos in the editor.')); + + +export const editorHintForeground = registerColor('editorHint.foreground', + { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hcDark: null, hcLight: null }, + nls.localize('editorHint.foreground', 'Foreground color of hint squigglies in the editor.')); + +export const editorHintBorder = registerColor('editorHint.border', + { dark: null, light: null, hcDark: Color.fromHex('#eeeeee').transparent(0.8), hcLight: '#292929' }, + nls.localize('hintBorder', 'If set, color of double underlines for hints in the editor.')); + + +export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', + { dark: '#4E94CE', light: Color.blue, hcDark: Color.cyan, hcLight: '#292929' }, + nls.localize('activeLinkForeground', 'Color of active links.')); + + +// ----- editor selection + +export const editorSelectionBackground = registerColor('editor.selectionBackground', + { light: '#ADD6FF', dark: '#264F78', hcDark: '#f3f518', hcLight: '#0F4A85' }, + nls.localize('editorSelectionBackground', "Color of the editor selection.")); + +export const editorSelectionForeground = registerColor('editor.selectionForeground', + { light: null, dark: null, hcDark: '#000000', hcLight: Color.white }, + nls.localize('editorSelectionForeground', "Color of the selected text for high contrast.")); + +export const editorInactiveSelection = registerColor('editor.inactiveSelectionBackground', + { light: transparent(editorSelectionBackground, 0.5), dark: transparent(editorSelectionBackground, 0.5), hcDark: transparent(editorSelectionBackground, 0.7), hcLight: transparent(editorSelectionBackground, 0.5) }, + nls.localize('editorInactiveSelection', "Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations."), true); + +export const editorSelectionHighlight = registerColor('editor.selectionHighlightBackground', + { light: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), dark: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), hcDark: null, hcLight: null }, + nls.localize('editorSelectionHighlight', 'Color for regions with the same content as the selection. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const editorSelectionHighlightBorder = registerColor('editor.selectionHighlightBorder', + { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, + nls.localize('editorSelectionHighlightBorder', "Border color for regions with the same content as the selection.")); + + +// ----- editor find + +export const editorFindMatch = registerColor('editor.findMatchBackground', + { light: '#A8AC94', dark: '#515C6A', hcDark: null, hcLight: null }, + nls.localize('editorFindMatch', "Color of the current search match.")); + +export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', + { light: '#EA5C0055', dark: '#EA5C0055', hcDark: null, hcLight: null }, + nls.localize('findMatchHighlight', "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations."), true); + +export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', + { dark: '#3a3d4166', light: '#b4b4b44d', hcDark: null, hcLight: null }, + nls.localize('findRangeHighlight', "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); + +export const editorFindMatchBorder = registerColor('editor.findMatchBorder', + { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, + nls.localize('editorFindMatchBorder', "Border color of the current search match.")); + +export const editorFindMatchHighlightBorder = registerColor('editor.findMatchHighlightBorder', + { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, + nls.localize('findMatchHighlightBorder', "Border color of the other search matches.")); + +export const editorFindRangeHighlightBorder = registerColor('editor.findRangeHighlightBorder', + { dark: null, light: null, hcDark: transparent(activeContrastBorder, 0.4), hcLight: transparent(activeContrastBorder, 0.4) }, + nls.localize('findRangeHighlightBorder', "Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); + + +// ----- editor hover + +export const editorHoverHighlight = registerColor('editor.hoverHighlightBackground', + { light: '#ADD6FF26', dark: '#264f7840', hcDark: '#ADD6FF26', hcLight: null }, + nls.localize('hoverHighlight', 'Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const editorHoverBackground = registerColor('editorHoverWidget.background', + { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + nls.localize('hoverBackground', 'Background color of the editor hover.')); + +export const editorHoverForeground = registerColor('editorHoverWidget.foreground', + { light: editorWidgetForeground, dark: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + nls.localize('hoverForeground', 'Foreground color of the editor hover.')); + +export const editorHoverBorder = registerColor('editorHoverWidget.border', + { light: editorWidgetBorder, dark: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, + nls.localize('hoverBorder', 'Border color of the editor hover.')); + +export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.statusBarBackground', + { dark: lighten(editorHoverBackground, 0.2), light: darken(editorHoverBackground, 0.05), hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + nls.localize('statusBarBackground', "Background color of the editor hover status bar.")); + + +// ----- editor inlay hint + +export const editorInlayHintForeground = registerColor('editorInlayHint.foreground', + { dark: '#969696', light: '#969696', hcDark: Color.white, hcLight: Color.black }, + nls.localize('editorInlayHintForeground', 'Foreground color of inline hints')); + +export const editorInlayHintBackground = registerColor('editorInlayHint.background', + { dark: transparent(badgeBackground, .10), light: transparent(badgeBackground, .10), hcDark: transparent(Color.white, .10), hcLight: transparent(badgeBackground, .10) }, + nls.localize('editorInlayHintBackground', 'Background color of inline hints')); + +export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', + { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); + +export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', + { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); + +export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', + { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); + +export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', + { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); + + +// ----- editor lightbulb + +export const editorLightBulbForeground = registerColor('editorLightBulb.foreground', + { dark: '#FFCC00', light: '#DDB100', hcDark: '#FFCC00', hcLight: '#007ACC' }, + nls.localize('editorLightBulbForeground', "The color used for the lightbulb actions icon.")); + +export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAutoFix.foreground', + { dark: '#75BEFF', light: '#007ACC', hcDark: '#75BEFF', hcLight: '#007ACC' }, + nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); + +export const editorLightBulbAiForeground = registerColor('editorLightBulbAi.foreground', + { dark: editorLightBulbForeground, light: editorLightBulbForeground, hcDark: editorLightBulbForeground, hcLight: editorLightBulbForeground }, + nls.localize('editorLightBulbAiForeground', "The color used for the lightbulb AI icon.")); + + +// ----- editor snippet + +export const snippetTabstopHighlightBackground = registerColor('editor.snippetTabstopHighlightBackground', + { dark: new Color(new RGBA(124, 124, 124, 0.3)), light: new Color(new RGBA(10, 50, 100, 0.2)), hcDark: new Color(new RGBA(124, 124, 124, 0.3)), hcLight: new Color(new RGBA(10, 50, 100, 0.2)) }, + nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); + +export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); + +export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); + +export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', + { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hcDark: '#525252', hcLight: '#292929' }, + nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final tabstop of a snippet.")); + + +// ----- diff editor + +export const defaultInsertColor = new Color(new RGBA(155, 185, 85, .2)); +export const defaultRemoveColor = new Color(new RGBA(255, 0, 0, .2)); + +export const diffInserted = registerColor('diffEditor.insertedTextBackground', + { dark: '#9ccc2c33', light: '#9ccc2c40', hcDark: null, hcLight: null }, + nls.localize('diffEditorInserted', 'Background color for text that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const diffRemoved = registerColor('diffEditor.removedTextBackground', + { dark: '#ff000033', light: '#ff000033', hcDark: null, hcLight: null }, + nls.localize('diffEditorRemoved', 'Background color for text that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); + + +export const diffInsertedLine = registerColor('diffEditor.insertedLineBackground', + { dark: defaultInsertColor, light: defaultInsertColor, hcDark: null, hcLight: null }, + nls.localize('diffEditorInsertedLines', 'Background color for lines that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const diffRemovedLine = registerColor('diffEditor.removedLineBackground', + { dark: defaultRemoveColor, light: defaultRemoveColor, hcDark: null, hcLight: null }, + nls.localize('diffEditorRemovedLines', 'Background color for lines that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); + + +export const diffInsertedLineGutter = registerColor('diffEditorGutter.insertedLineBackground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('diffEditorInsertedLineGutter', 'Background color for the margin where lines got inserted.')); + +export const diffRemovedLineGutter = registerColor('diffEditorGutter.removedLineBackground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('diffEditorRemovedLineGutter', 'Background color for the margin where lines got removed.')); + + +export const diffOverviewRulerInserted = registerColor('diffEditorOverview.insertedForeground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('diffEditorOverviewInserted', 'Diff overview ruler foreground for inserted content.')); + +export const diffOverviewRulerRemoved = registerColor('diffEditorOverview.removedForeground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('diffEditorOverviewRemoved', 'Diff overview ruler foreground for removed content.')); + + +export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder', + { dark: null, light: null, hcDark: '#33ff2eff', hcLight: '#374E06' }, + nls.localize('diffEditorInsertedOutline', 'Outline color for the text that got inserted.')); + +export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', + { dark: null, light: null, hcDark: '#FF008F', hcLight: '#AD0707' }, + nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.')); + + +export const diffBorder = registerColor('diffEditor.border', + { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('diffEditorBorder', 'Border color between the two text editors.')); + +export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', + { dark: '#cccccc33', light: '#22222233', hcDark: null, hcLight: null }, + nls.localize('diffDiagonalFill', "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views.")); + + +export const diffUnchangedRegionBackground = registerColor('diffEditor.unchangedRegionBackground', + { dark: 'sideBar.background', light: 'sideBar.background', hcDark: 'sideBar.background', hcLight: 'sideBar.background' }, + nls.localize('diffEditor.unchangedRegionBackground', "The background color of unchanged blocks in the diff editor.")); + +export const diffUnchangedRegionForeground = registerColor('diffEditor.unchangedRegionForeground', + { dark: 'foreground', light: 'foreground', hcDark: 'foreground', hcLight: 'foreground' }, + nls.localize('diffEditor.unchangedRegionForeground', "The foreground color of unchanged blocks in the diff editor.")); + +export const diffUnchangedTextBackground = registerColor('diffEditor.unchangedCodeBackground', + { dark: '#74747429', light: '#b8b8b829', hcDark: null, hcLight: null }, + nls.localize('diffEditor.unchangedCodeBackground', "The background color of unchanged code in the diff editor.")); + + +// ----- widget + +export const widgetShadow = registerColor('widget.shadow', + { dark: transparent(Color.black, .36), light: transparent(Color.black, .16), hcDark: null, hcLight: null }, + nls.localize('widgetShadow', 'Shadow color of widgets such as find/replace inside the editor.')); + +export const widgetBorder = registerColor('widget.border', + { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('widgetBorder', 'Border color of widgets such as find/replace inside the editor.')); + + +// ----- toolbar + +export const toolbarHoverBackground = registerColor('toolbar.hoverBackground', + { dark: '#5a5d5e50', light: '#b8b8b850', hcDark: null, hcLight: null }, + nls.localize('toolbarHoverBackground', "Toolbar background when hovering over actions using the mouse")); + +export const toolbarHoverOutline = registerColor('toolbar.hoverOutline', + { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, + nls.localize('toolbarHoverOutline', "Toolbar outline when hovering over actions using the mouse")); + +export const toolbarActiveBackground = registerColor('toolbar.activeBackground', + { dark: lighten(toolbarHoverBackground, 0.1), light: darken(toolbarHoverBackground, 0.1), hcDark: null, hcLight: null }, + nls.localize('toolbarActiveBackground', "Toolbar background when holding the mouse over actions")); + + +// ----- breadcumbs + +export const breadcrumbsForeground = registerColor('breadcrumb.foreground', + { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hcDark: transparent(foreground, 0.8), hcLight: transparent(foreground, 0.8) }, + nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); + +export const breadcrumbsBackground = registerColor('breadcrumb.background', + { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); + +export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', + { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, + nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); + +export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', + { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, + nls.localize('breadcrumbsSelectedForeground', "Color of selected breadcrumb items.")); + +export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', + { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); + + +// ----- merge + +const headerTransparency = 0.5; +const currentBaseColor = Color.fromHex('#40C8AE').transparent(headerTransparency); +const incomingBaseColor = Color.fromHex('#40A6FF').transparent(headerTransparency); +const commonBaseColor = Color.fromHex('#606060').transparent(0.4); +const contentTransparency = 0.4; +const rulerTransparency = 1; + +export const mergeCurrentHeaderBackground = registerColor('merge.currentHeaderBackground', + { dark: currentBaseColor, light: currentBaseColor, hcDark: null, hcLight: null }, + nls.localize('mergeCurrentHeaderBackground', 'Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const mergeCurrentContentBackground = registerColor('merge.currentContentBackground', + { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, + nls.localize('mergeCurrentContentBackground', 'Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeaderBackground', + { dark: incomingBaseColor, light: incomingBaseColor, hcDark: null, hcLight: null }, + nls.localize('mergeIncomingHeaderBackground', 'Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const mergeIncomingContentBackground = registerColor('merge.incomingContentBackground', + { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, + nls.localize('mergeIncomingContentBackground', 'Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBackground', + { dark: commonBaseColor, light: commonBaseColor, hcDark: null, hcLight: null }, + nls.localize('mergeCommonHeaderBackground', 'Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const mergeCommonContentBackground = registerColor('merge.commonContentBackground', + { dark: transparent(mergeCommonHeaderBackground, contentTransparency), light: transparent(mergeCommonHeaderBackground, contentTransparency), hcDark: transparent(mergeCommonHeaderBackground, contentTransparency), hcLight: transparent(mergeCommonHeaderBackground, contentTransparency) }, + nls.localize('mergeCommonContentBackground', 'Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const mergeBorder = registerColor('merge.border', + { dark: null, light: null, hcDark: '#C3DF6F', hcLight: '#007ACC' }, + nls.localize('mergeBorder', 'Border color on headers and the splitter in inline merge-conflicts.')); + + +export const overviewRulerCurrentContentForeground = registerColor('editorOverviewRuler.currentContentForeground', + { dark: transparent(mergeCurrentHeaderBackground, rulerTransparency), light: transparent(mergeCurrentHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, + nls.localize('overviewRulerCurrentContentForeground', 'Current overview ruler foreground for inline merge-conflicts.')); + +export const overviewRulerIncomingContentForeground = registerColor('editorOverviewRuler.incomingContentForeground', + { dark: transparent(mergeIncomingHeaderBackground, rulerTransparency), light: transparent(mergeIncomingHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, + nls.localize('overviewRulerIncomingContentForeground', 'Incoming overview ruler foreground for inline merge-conflicts.')); + +export const overviewRulerCommonContentForeground = registerColor('editorOverviewRuler.commonContentForeground', + { dark: transparent(mergeCommonHeaderBackground, rulerTransparency), light: transparent(mergeCommonHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, + nls.localize('overviewRulerCommonContentForeground', 'Common ancestor overview ruler foreground for inline merge-conflicts.')); + +export const overviewRulerFindMatchForeground = registerColor('editorOverviewRuler.findMatchForeground', + { dark: '#d186167e', light: '#d186167e', hcDark: '#AB5A00', hcLight: '' }, + nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); + +export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', + { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, + nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); + + +// ----- problems + +export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', + { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); + +export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', + { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); + +export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', + { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); diff --git a/code/src/vs/platform/theme/common/colors/inputColors.ts b/code/src/vs/platform/theme/common/colors/inputColors.ts new file mode 100644 index 00000000000..dc38222d402 --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/inputColors.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { Color, RGBA } from 'vs/base/common/color'; +import { registerColor, transparent, lighten, darken } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { foreground, contrastBorder, focusBorder, iconForeground } from 'vs/platform/theme/common/colors/baseColors'; +import { editorWidgetBackground } from 'vs/platform/theme/common/colors/editorColors'; + + +// ----- input + +export const inputBackground = registerColor('input.background', + { dark: '#3C3C3C', light: Color.white, hcDark: Color.black, hcLight: Color.white }, + nls.localize('inputBoxBackground', "Input box background.")); + +export const inputForeground = registerColor('input.foreground', + { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + nls.localize('inputBoxForeground', "Input box foreground.")); + +export const inputBorder = registerColor('input.border', + { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('inputBoxBorder', "Input box border.")); + +export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', + { dark: '#007ACC', light: '#007ACC', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields.")); + +export const inputActiveOptionHoverBackground = registerColor('inputOption.hoverBackground', + { dark: '#5a5d5e80', light: '#b8b8b850', hcDark: null, hcLight: null }, + nls.localize('inputOption.hoverBackground', "Background color of activated options in input fields.")); + +export const inputActiveOptionBackground = registerColor('inputOption.activeBackground', + { dark: transparent(focusBorder, 0.4), light: transparent(focusBorder, 0.2), hcDark: Color.transparent, hcLight: Color.transparent }, + nls.localize('inputOption.activeBackground', "Background hover color of options in input fields.")); + +export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', + { dark: Color.white, light: Color.black, hcDark: foreground, hcLight: foreground }, + nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields.")); + +export const inputPlaceholderForeground = registerColor('input.placeholderForeground', + { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, + nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); + + +// ----- input validation + +export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', + { dark: '#063B49', light: '#D6ECF2', hcDark: Color.black, hcLight: Color.white }, + nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); + +export const inputValidationInfoForeground = registerColor('inputValidation.infoForeground', + { dark: null, light: null, hcDark: null, hcLight: foreground }, + nls.localize('inputValidationInfoForeground', "Input validation foreground color for information severity.")); + +export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', + { dark: '#007acc', light: '#007acc', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('inputValidationInfoBorder', "Input validation border color for information severity.")); + +export const inputValidationWarningBackground = registerColor('inputValidation.warningBackground', + { dark: '#352A05', light: '#F6F5D2', hcDark: Color.black, hcLight: Color.white }, + nls.localize('inputValidationWarningBackground', "Input validation background color for warning severity.")); + +export const inputValidationWarningForeground = registerColor('inputValidation.warningForeground', + { dark: null, light: null, hcDark: null, hcLight: foreground }, + nls.localize('inputValidationWarningForeground', "Input validation foreground color for warning severity.")); + +export const inputValidationWarningBorder = registerColor('inputValidation.warningBorder', + { dark: '#B89500', light: '#B89500', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('inputValidationWarningBorder', "Input validation border color for warning severity.")); + +export const inputValidationErrorBackground = registerColor('inputValidation.errorBackground', + { dark: '#5A1D1D', light: '#F2DEDE', hcDark: Color.black, hcLight: Color.white }, + nls.localize('inputValidationErrorBackground', "Input validation background color for error severity.")); + +export const inputValidationErrorForeground = registerColor('inputValidation.errorForeground', + { dark: null, light: null, hcDark: null, hcLight: foreground }, + nls.localize('inputValidationErrorForeground', "Input validation foreground color for error severity.")); + +export const inputValidationErrorBorder = registerColor('inputValidation.errorBorder', + { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('inputValidationErrorBorder', "Input validation border color for error severity.")); + + +// ----- select + +export const selectBackground = registerColor('dropdown.background', + { dark: '#3C3C3C', light: Color.white, hcDark: Color.black, hcLight: Color.white }, + nls.localize('dropdownBackground', "Dropdown background.")); + +export const selectListBackground = registerColor('dropdown.listBackground', + { dark: null, light: null, hcDark: Color.black, hcLight: Color.white }, + nls.localize('dropdownListBackground', "Dropdown list background.")); + +export const selectForeground = registerColor('dropdown.foreground', + { dark: '#F0F0F0', light: foreground, hcDark: Color.white, hcLight: foreground }, + nls.localize('dropdownForeground', "Dropdown foreground.")); + +export const selectBorder = registerColor('dropdown.border', + { dark: selectBackground, light: '#CECECE', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('dropdownBorder', "Dropdown border.")); + + +// ------ button + +export const buttonForeground = registerColor('button.foreground', + { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, + nls.localize('buttonForeground', "Button foreground color.")); + +export const buttonSeparator = registerColor('button.separator', + { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: transparent(buttonForeground, .4), hcLight: transparent(buttonForeground, .4) }, + nls.localize('buttonSeparator', "Button separator color.")); + +export const buttonBackground = registerColor('button.background', + { dark: '#0E639C', light: '#007ACC', hcDark: null, hcLight: '#0F4A85' }, + nls.localize('buttonBackground', "Button background color.")); + +export const buttonHoverBackground = registerColor('button.hoverBackground', + { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hcDark: buttonBackground, hcLight: buttonBackground }, + nls.localize('buttonHoverBackground', "Button background color when hovering.")); + +export const buttonBorder = registerColor('button.border', + { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('buttonBorder', "Button border color.")); + +export const buttonSecondaryForeground = registerColor('button.secondaryForeground', + { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: foreground }, + nls.localize('buttonSecondaryForeground', "Secondary button foreground color.")); + +export const buttonSecondaryBackground = registerColor('button.secondaryBackground', + { dark: '#3A3D41', light: '#5F6A79', hcDark: null, hcLight: Color.white }, + nls.localize('buttonSecondaryBackground', "Secondary button background color.")); + +export const buttonSecondaryHoverBackground = registerColor('button.secondaryHoverBackground', + { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hcDark: null, hcLight: null }, + nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); + + +// ------ checkbox + +export const checkboxBackground = registerColor('checkbox.background', + { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + nls.localize('checkbox.background', "Background color of checkbox widget.")); + +export const checkboxSelectBackground = registerColor('checkbox.selectBackground', + { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + nls.localize('checkbox.select.background', "Background color of checkbox widget when the element it's in is selected.")); + +export const checkboxForeground = registerColor('checkbox.foreground', + { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + nls.localize('checkbox.foreground', "Foreground color of checkbox widget.")); + +export const checkboxBorder = registerColor('checkbox.border', + { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, + nls.localize('checkbox.border', "Border color of checkbox widget.")); + +export const checkboxSelectBorder = registerColor('checkbox.selectBorder', + { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, + nls.localize('checkbox.select.border', "Border color of checkbox widget when the element it's in is selected.")); + + +// ------ keybinding label + +export const keybindingLabelBackground = registerColor('keybindingLabel.background', + { dark: new Color(new RGBA(128, 128, 128, 0.17)), light: new Color(new RGBA(221, 221, 221, 0.4)), hcDark: Color.transparent, hcLight: Color.transparent }, + nls.localize('keybindingLabelBackground', "Keybinding label background color. The keybinding label is used to represent a keyboard shortcut.")); + +export const keybindingLabelForeground = registerColor('keybindingLabel.foreground', + { dark: Color.fromHex('#CCCCCC'), light: Color.fromHex('#555555'), hcDark: Color.white, hcLight: foreground }, + nls.localize('keybindingLabelForeground', "Keybinding label foreground color. The keybinding label is used to represent a keyboard shortcut.")); + +export const keybindingLabelBorder = registerColor('keybindingLabel.border', + { dark: new Color(new RGBA(51, 51, 51, 0.6)), light: new Color(new RGBA(204, 204, 204, 0.4)), hcDark: new Color(new RGBA(111, 195, 223)), hcLight: contrastBorder }, + nls.localize('keybindingLabelBorder', "Keybinding label border color. The keybinding label is used to represent a keyboard shortcut.")); + +export const keybindingLabelBottomBorder = registerColor('keybindingLabel.bottomBorder', + { dark: new Color(new RGBA(68, 68, 68, 0.6)), light: new Color(new RGBA(187, 187, 187, 0.4)), hcDark: new Color(new RGBA(111, 195, 223)), hcLight: foreground }, + nls.localize('keybindingLabelBottomBorder', "Keybinding label border bottom color. The keybinding label is used to represent a keyboard shortcut.")); diff --git a/code/src/vs/platform/theme/common/colors/listColors.ts b/code/src/vs/platform/theme/common/colors/listColors.ts new file mode 100644 index 00000000000..868230d356c --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/listColors.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { Color } from 'vs/base/common/color'; +import { registerColor, darken, lighten, ifDefinedThenElse, transparent } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { foreground, contrastBorder, activeContrastBorder, focusBorder, iconForeground } from 'vs/platform/theme/common/colors/baseColors'; +import { editorWidgetBackground, editorFindMatchHighlightBorder, editorFindMatchHighlight, widgetShadow } from 'vs/platform/theme/common/colors/editorColors'; + + +export const listFocusBackground = registerColor('list.focusBackground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); + +export const listFocusForeground = registerColor('list.focusForeground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); + +export const listFocusOutline = registerColor('list.focusOutline', + { dark: focusBorder, light: focusBorder, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, + nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); + +export const listFocusAndSelectionOutline = registerColor('list.focusAndSelectionOutline', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listFocusAndSelectionOutline', "List/Tree outline color for the focused item when the list/tree is active and selected. An active list/tree has keyboard focus, an inactive does not.")); + +export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', + { dark: '#04395E', light: '#0060C0', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, + nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); + +export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', + { dark: Color.white, light: Color.white, hcDark: null, hcLight: null }, + nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); + +export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); + +export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', + { dark: '#37373D', light: '#E4E6F1', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, + nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); + +export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); + +export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); + +export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); + +export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); + +export const listHoverBackground = registerColor('list.hoverBackground', + { dark: '#2A2D2E', light: '#F0F0F0', hcDark: Color.white.transparent(0.1), hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, + nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); + +export const listHoverForeground = registerColor('list.hoverForeground', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); + +export const listDropOverBackground = registerColor('list.dropBackground', + { dark: '#062F4A', light: '#D6EBFF', hcDark: null, hcLight: null }, + nls.localize('listDropBackground', "List/Tree drag and drop background when moving items over other items when using the mouse.")); + +export const listDropBetweenBackground = registerColor('list.dropBetweenBackground', + { dark: iconForeground, light: iconForeground, hcDark: null, hcLight: null }, + nls.localize('listDropBetweenBackground', "List/Tree drag and drop border color when moving items between items when using the mouse.")); + +export const listHighlightForeground = registerColor('list.highlightForeground', + { dark: '#2AAAFF', light: '#0066BF', hcDark: focusBorder, hcLight: focusBorder }, + nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); + +export const listFocusHighlightForeground = registerColor('list.focusHighlightForeground', + { dark: listHighlightForeground, light: ifDefinedThenElse(listActiveSelectionBackground, listHighlightForeground, '#BBE7FF'), hcDark: listHighlightForeground, hcLight: listHighlightForeground }, + nls.localize('listFocusHighlightForeground', 'List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.')); + +export const listInvalidItemForeground = registerColor('list.invalidItemForeground', + { dark: '#B89500', light: '#B89500', hcDark: '#B89500', hcLight: '#B5200D' }, + nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.')); + +export const listErrorForeground = registerColor('list.errorForeground', + { dark: '#F88070', light: '#B01011', hcDark: null, hcLight: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); + +export const listWarningForeground = registerColor('list.warningForeground', + { dark: '#CCA700', light: '#855F00', hcDark: null, hcLight: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.')); + +export const listFilterWidgetBackground = registerColor('listFilterWidget.background', + { light: darken(editorWidgetBackground, 0), dark: lighten(editorWidgetBackground, 0), hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + nls.localize('listFilterWidgetBackground', 'Background color of the type filter widget in lists and trees.')); + +export const listFilterWidgetOutline = registerColor('listFilterWidget.outline', + { dark: Color.transparent, light: Color.transparent, hcDark: '#f38518', hcLight: '#007ACC' }, + nls.localize('listFilterWidgetOutline', 'Outline color of the type filter widget in lists and trees.')); + +export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget.noMatchesOutline', + { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); + +export const listFilterWidgetShadow = registerColor('listFilterWidget.shadow', + { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, + nls.localize('listFilterWidgetShadow', 'Shadow color of the type filter widget in lists and trees.')); + +export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', + { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hcDark: null, hcLight: null }, + nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); + +export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', + { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hcDark: contrastBorder, hcLight: activeContrastBorder }, + nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); + +export const listDeemphasizedForeground = registerColor('list.deemphasizedForeground', + { dark: '#8C8C8C', light: '#8E8E90', hcDark: '#A7A8A9', hcLight: '#666666' }, + nls.localize('listDeemphasizedForeground', "List/Tree foreground color for items that are deemphasized.")); + + +// ------ tree + +export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', + { dark: '#585858', light: '#a9a9a9', hcDark: '#a9a9a9', hcLight: '#a5a5a5' }, + nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); + +export const treeInactiveIndentGuidesStroke = registerColor('tree.inactiveIndentGuidesStroke', + { dark: transparent(treeIndentGuidesStroke, 0.4), light: transparent(treeIndentGuidesStroke, 0.4), hcDark: transparent(treeIndentGuidesStroke, 0.4), hcLight: transparent(treeIndentGuidesStroke, 0.4) }, + nls.localize('treeInactiveIndentGuidesStroke', "Tree stroke color for the indentation guides that are not active.")); + + +// ------ table + +export const tableColumnsBorder = registerColor('tree.tableColumnsBorder', + { dark: '#CCCCCC20', light: '#61616120', hcDark: null, hcLight: null }, + nls.localize('tableColumnsBorder', "Table border color between columns.")); + +export const tableOddRowsBackgroundColor = registerColor('tree.tableOddRowsBackground', + { dark: transparent(foreground, 0.04), light: transparent(foreground, 0.04), hcDark: null, hcLight: null }, + nls.localize('tableOddRowsBackgroundColor', "Background color for odd table rows.")); diff --git a/code/src/vs/platform/theme/common/colors/menuColors.ts b/code/src/vs/platform/theme/common/colors/menuColors.ts new file mode 100644 index 00000000000..6fa9a0ec326 --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/menuColors.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { registerColor } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colors/baseColors'; +import { selectForeground, selectBackground } from 'vs/platform/theme/common/colors/inputColors'; +import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colors/listColors'; + + +export const menuBorder = registerColor('menu.border', + { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('menuBorder', "Border color of menus.")); + +export const menuForeground = registerColor('menu.foreground', + { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + nls.localize('menuForeground', "Foreground color of menu items.")); + +export const menuBackground = registerColor('menu.background', + { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + nls.localize('menuBackground', "Background color of menu items.")); + +export const menuSelectionForeground = registerColor('menu.selectionForeground', + { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); + +export const menuSelectionBackground = registerColor('menu.selectionBackground', + { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hcDark: listActiveSelectionBackground, hcLight: listActiveSelectionBackground }, + nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); + +export const menuSelectionBorder = registerColor('menu.selectionBorder', + { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, + nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); + +export const menuSeparatorBackground = registerColor('menu.separatorBackground', + { dark: '#606060', light: '#D4D4D4', hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('menuSeparatorBackground', "Color of a separator menu item in menus.")); diff --git a/code/src/vs/platform/theme/common/colors/minimapColors.ts b/code/src/vs/platform/theme/common/colors/minimapColors.ts new file mode 100644 index 00000000000..0b051994d09 --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/minimapColors.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { Color, RGBA } from 'vs/base/common/color'; +import { registerColor, transparent } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { editorInfoForeground, editorWarningForeground, editorWarningBorder, editorInfoBorder } from 'vs/platform/theme/common/colors/editorColors'; +import { scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground } from 'vs/platform/theme/common/colors/miscColors'; + + +export const minimapFindMatch = registerColor('minimap.findMatchHighlight', + { light: '#d18616', dark: '#d18616', hcDark: '#AB5A00', hcLight: '#0F4A85' }, + nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true); + +export const minimapSelectionOccurrenceHighlight = registerColor('minimap.selectionOccurrenceHighlight', + { light: '#c9c9c9', dark: '#676767', hcDark: '#ffffff', hcLight: '#0F4A85' }, + nls.localize('minimapSelectionOccurrenceHighlight', 'Minimap marker color for repeating editor selections.'), true); + +export const minimapSelection = registerColor('minimap.selectionHighlight', + { light: '#ADD6FF', dark: '#264F78', hcDark: '#ffffff', hcLight: '#0F4A85' }, + nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true); + +export const minimapInfo = registerColor('minimap.infoHighlight', + { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoBorder, hcLight: editorInfoBorder }, + nls.localize('minimapInfo', 'Minimap marker color for infos.')); + +export const minimapWarning = registerColor('minimap.warningHighlight', + { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningBorder, hcLight: editorWarningBorder }, + nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); + +export const minimapError = registerColor('minimap.errorHighlight', + { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '#B5200D' }, + nls.localize('minimapError', 'Minimap marker color for errors.')); + +export const minimapBackground = registerColor('minimap.background', + { dark: null, light: null, hcDark: null, hcLight: null }, + nls.localize('minimapBackground', "Minimap background color.")); + +export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', + { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hcDark: Color.fromHex('#000f'), hcLight: Color.fromHex('#000f') }, + nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); + +export const minimapSliderBackground = registerColor('minimapSlider.background', + { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hcDark: transparent(scrollbarSliderBackground, 0.5), hcLight: transparent(scrollbarSliderBackground, 0.5) }, + nls.localize('minimapSliderBackground', "Minimap slider background color.")); + +export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', + { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hcDark: transparent(scrollbarSliderHoverBackground, 0.5), hcLight: transparent(scrollbarSliderHoverBackground, 0.5) }, + nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); + +export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', + { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hcDark: transparent(scrollbarSliderActiveBackground, 0.5), hcLight: transparent(scrollbarSliderActiveBackground, 0.5) }, + nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); diff --git a/code/src/vs/platform/theme/common/colors/miscColors.ts b/code/src/vs/platform/theme/common/colors/miscColors.ts new file mode 100644 index 00000000000..5a2ea49b702 --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/miscColors.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { Color } from 'vs/base/common/color'; +import { registerColor, transparent } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colors/baseColors'; + + +// ----- sash + +export const sashHoverBorder = registerColor('sash.hoverBorder', + { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, + nls.localize('sashActiveBorder', "Border color of active sashes.")); + + +// ----- badge + +export const badgeBackground = registerColor('badge.background', + { dark: '#4D4D4D', light: '#C4C4C4', hcDark: Color.black, hcLight: '#0F4A85' }, + nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); + +export const badgeForeground = registerColor('badge.foreground', + { dark: Color.white, light: '#333', hcDark: Color.white, hcLight: Color.white }, + nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count.")); + + +// ----- scrollbar + +export const scrollbarShadow = registerColor('scrollbar.shadow', + { dark: '#000000', light: '#DDDDDD', hcDark: null, hcLight: null }, + nls.localize('scrollbarShadow', "Scrollbar shadow to indicate that the view is scrolled.")); + +export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', + { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hcDark: transparent(contrastBorder, 0.6), hcLight: transparent(contrastBorder, 0.4) }, + nls.localize('scrollbarSliderBackground', "Scrollbar slider background color.")); + +export const scrollbarSliderHoverBackground = registerColor('scrollbarSlider.hoverBackground', + { dark: Color.fromHex('#646464').transparent(0.7), light: Color.fromHex('#646464').transparent(0.7), hcDark: transparent(contrastBorder, 0.8), hcLight: transparent(contrastBorder, 0.8) }, + nls.localize('scrollbarSliderHoverBackground', "Scrollbar slider background color when hovering.")); + +export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.activeBackground', + { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('scrollbarSliderActiveBackground', "Scrollbar slider background color when clicked on.")); + + +// ----- progress bar + +export const progressBarBackground = registerColor('progressBar.background', + { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); diff --git a/code/src/vs/platform/theme/common/colors/quickpickColors.ts b/code/src/vs/platform/theme/common/colors/quickpickColors.ts new file mode 100644 index 00000000000..7f8fc271a6e --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/quickpickColors.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 * as nls from 'vs/nls'; + +// Import the effects we need +import { Color, RGBA } from 'vs/base/common/color'; +import { registerColor, oneOf } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { editorWidgetBackground, editorWidgetForeground } from 'vs/platform/theme/common/colors/editorColors'; +import { listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground } from 'vs/platform/theme/common/colors/listColors'; + + +export const quickInputBackground = registerColor('quickInput.background', + { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); + +export const quickInputForeground = registerColor('quickInput.foreground', + { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); + +export const quickInputTitleBackground = registerColor('quickInputTitle.background', + { dark: new Color(new RGBA(255, 255, 255, 0.105)), light: new Color(new RGBA(0, 0, 0, 0.06)), hcDark: '#000000', hcLight: Color.white }, + nls.localize('pickerTitleBackground', "Quick picker title background color. The quick picker widget is the container for pickers like the command palette.")); + +export const pickerGroupForeground = registerColor('pickerGroup.foreground', + { dark: '#3794FF', light: '#0066BF', hcDark: Color.white, hcLight: '#0F4A85' }, + nls.localize('pickerGroupForeground', "Quick picker color for grouping labels.")); + +export const pickerGroupBorder = registerColor('pickerGroup.border', + { dark: '#3F3F46', light: '#CCCEDB', hcDark: Color.white, hcLight: '#0F4A85' }, + nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); + +export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', + { dark: null, light: null, hcDark: null, hcLight: null }, '', undefined, + nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); + +export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', + { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); + +export const quickInputListFocusIconForeground = registerColor('quickInputList.focusIconForeground', + { dark: listActiveSelectionIconForeground, light: listActiveSelectionIconForeground, hcDark: listActiveSelectionIconForeground, hcLight: listActiveSelectionIconForeground }, + nls.localize('quickInput.listFocusIconForeground', "Quick picker icon foreground color for the focused item.")); + +export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', + { dark: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), light: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), hcDark: null, hcLight: null }, + nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); diff --git a/code/src/vs/platform/theme/common/colors/searchColors.ts b/code/src/vs/platform/theme/common/colors/searchColors.ts new file mode 100644 index 00000000000..8f10c53ab0e --- /dev/null +++ b/code/src/vs/platform/theme/common/colors/searchColors.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * 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 the effects we need +import { registerColor, transparent } from 'vs/platform/theme/common/colorUtils'; + +// Import the colors we need +import { foreground } from 'vs/platform/theme/common/colors/baseColors'; +import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from 'vs/platform/theme/common/colors/editorColors'; + + +export const searchResultsInfoForeground = registerColor('search.resultsInfoForeground', + { light: foreground, dark: transparent(foreground, 0.65), hcDark: foreground, hcLight: foreground }, + nls.localize('search.resultsInfoForeground', "Color of the text in the search viewlet's completion message.")); + + +// ----- search editor (Distinct from normal editor find match to allow for better differentiation) + +export const searchEditorFindMatch = registerColor('searchEditor.findMatchBackground', + { light: transparent(editorFindMatchHighlight, 0.66), dark: transparent(editorFindMatchHighlight, 0.66), hcDark: editorFindMatchHighlight, hcLight: editorFindMatchHighlight }, + nls.localize('searchEditor.queryMatch', "Color of the Search Editor query matches.")); + +export const searchEditorFindMatchBorder = registerColor('searchEditor.findMatchBorder', + { light: transparent(editorFindMatchHighlightBorder, 0.66), dark: transparent(editorFindMatchHighlightBorder, 0.66), hcDark: editorFindMatchHighlightBorder, hcLight: editorFindMatchHighlightBorder }, + nls.localize('searchEditor.editorFindMatchBorder', "Border color of the Search Editor query matches.")); diff --git a/code/src/vs/platform/theme/common/iconRegistry.ts b/code/src/vs/platform/theme/common/iconRegistry.ts index 282230adee5..214f4846dcc 100644 --- a/code/src/vs/platform/theme/common/iconRegistry.ts +++ b/code/src/vs/platform/theme/common/iconRegistry.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from 'vs/base/common/async'; -import { Codicon, getCodiconFontCharacters } from 'vs/base/common/codicons'; +import { Codicon } from 'vs/base/common/codicons'; +import { getCodiconFontCharacters } from 'vs/base/common/codiconsUtil'; import { ThemeIcon, IconIdentifier } from 'vs/base/common/themables'; import { Emitter, Event } from 'vs/base/common/event'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; diff --git a/code/src/vs/platform/tunnel/common/tunnel.ts b/code/src/vs/platform/tunnel/common/tunnel.ts index 62c9059d2f8..86b4da4b409 100644 --- a/code/src/vs/platform/tunnel/common/tunnel.ts +++ b/code/src/vs/platform/tunnel/common/tunnel.ts @@ -155,6 +155,23 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: }; } +export function extractQueryLocalHostUriMetaDataForPortMapping(uri: URI): { address: string; port: number } | undefined { + if (uri.scheme !== 'http' && uri.scheme !== 'https' || !uri.query) { + return undefined; + } + const keyvalues = uri.query.split('&'); + for (const keyvalue of keyvalues) { + const value = keyvalue.split('=')[1]; + if (/^https?:/.exec(value)) { + const result = extractLocalHostUriMetaDataForPortMapping(URI.parse(value)); + if (result) { + return result; + } + } + } + return undefined; +} + export const LOCALHOST_ADDRESSES = ['localhost', '127.0.0.1', '0:0:0:0:0:0:0:1', '::1']; export function isLocalhost(host: string): boolean { return LOCALHOST_ADDRESSES.indexOf(host) >= 0; diff --git a/code/src/vs/platform/tunnel/test/common/tunnel.test.ts b/code/src/vs/platform/tunnel/test/common/tunnel.test.ts new file mode 100644 index 00000000000..d86d3f47bd7 --- /dev/null +++ b/code/src/vs/platform/tunnel/test/common/tunnel.test.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. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { + extractLocalHostUriMetaDataForPortMapping, + extractQueryLocalHostUriMetaDataForPortMapping +} from 'vs/platform/tunnel/common/tunnel'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; + + +suite('Tunnel', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function portMappingDoTest(uri: string, + func: (uri: URI) => { address: string; port: number } | undefined, + expectedAddress?: string, + expectedPort?: number) { + const res = func(URI.parse(uri)); + assert.strictEqual(!expectedAddress, !res); + assert.strictEqual(res?.address, expectedAddress); + assert.strictEqual(res?.port, expectedPort); + } + + function portMappingTest(uri: string, expectedAddress?: string, expectedPort?: number) { + portMappingDoTest(uri, extractLocalHostUriMetaDataForPortMapping, expectedAddress, expectedPort); + } + + function portMappingTestQuery(uri: string, expectedAddress?: string, expectedPort?: number) { + portMappingDoTest(uri, extractQueryLocalHostUriMetaDataForPortMapping, expectedAddress, expectedPort); + } + + test('portMapping', () => { + portMappingTest('file:///foo.bar/baz'); + portMappingTest('http://foo.bar:1234'); + portMappingTest('http://localhost:8080', 'localhost', 8080); + portMappingTest('https://localhost:443', 'localhost', 443); + portMappingTest('http://127.0.0.1:3456', '127.0.0.1', 3456); + portMappingTest('http://0.0.0.0:7654', '0.0.0.0', 7654); + portMappingTest('http://localhost:8080/path?foo=bar', 'localhost', 8080); + portMappingTest('http://localhost:8080/path?foo=http%3A%2F%2Flocalhost%3A8081', 'localhost', 8080); + portMappingTestQuery('http://foo.bar/path?url=http%3A%2F%2Flocalhost%3A8081', 'localhost', 8081); + portMappingTestQuery('http://foo.bar/path?url=http%3A%2F%2Flocalhost%3A8081&url2=http%3A%2F%2Flocalhost%3A8082', 'localhost', 8081); + portMappingTestQuery('http://foo.bar/path?url=http%3A%2F%2Fmicrosoft.com%2Fbad&url2=http%3A%2F%2Flocalhost%3A8081', 'localhost', 8081); + }); +}); diff --git a/code/src/vs/platform/update/common/update.ts b/code/src/vs/platform/update/common/update.ts index 4cc8994bd01..73e7d7afffe 100644 --- a/code/src/vs/platform/update/common/update.ts +++ b/code/src/vs/platform/update/common/update.ts @@ -7,8 +7,10 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IUpdate { + // Windows and Linux: 9a19815253d91900be5ec1016e0ecc7cc9a6950 (Commit Hash). Mac: 1.54.0 (Product Version) version: string; - productVersion: string; + productVersion?: string; + timestamp?: number; url?: string; sha256hash?: string; } @@ -63,7 +65,7 @@ export type Disabled = { type: StateType.Disabled; reason: DisablementReason }; export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean }; export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate }; -export type Downloading = { type: StateType.Downloading; update: IUpdate }; +export type Downloading = { type: StateType.Downloading }; export type Downloaded = { type: StateType.Downloaded; update: IUpdate }; export type Updating = { type: StateType.Updating; update: IUpdate }; export type Ready = { type: StateType.Ready; update: IUpdate }; @@ -76,7 +78,7 @@ export const State = { Idle: (updateType: UpdateType, error?: string) => ({ type: StateType.Idle, updateType, error }) as Idle, CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), - Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), + Downloading: { type: StateType.Downloading } as Downloading, Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), Updating: (update: IUpdate) => ({ type: StateType.Updating, update } as Updating), Ready: (update: IUpdate) => ({ type: StateType.Ready, update } as Ready), diff --git a/code/src/vs/platform/update/electron-main/updateService.darwin.ts b/code/src/vs/platform/update/electron-main/updateService.darwin.ts index 329488abb51..183c69da906 100644 --- a/code/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/code/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -24,8 +24,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } - @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version) => ({ url, version, productVersion: version })); } - @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, date) => ({ releaseNotes, version, productVersion: version, date })); } + @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @@ -96,12 +96,12 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau electron.autoUpdater.checkForUpdates(); } - private onUpdateAvailable(update: IUpdate): void { + private onUpdateAvailable(): void { if (this.state.type !== StateType.CheckingForUpdates) { return; } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); } private onUpdateDownloaded(update: IUpdate): void { @@ -109,6 +109,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return; } + this.setState(State.Downloaded(update)); + type UpdateDownloadedClassification = { owner: 'joaomoreno'; version: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version number of the new VS Code that has been downloaded.' }; diff --git a/code/src/vs/platform/update/electron-main/updateService.snap.ts b/code/src/vs/platform/update/electron-main/updateService.snap.ts index cf54be65d45..c20ce198e0c 100644 --- a/code/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/code/src/vs/platform/update/electron-main/updateService.snap.ts @@ -165,7 +165,7 @@ export class SnapUpdateService extends AbstractUpdateService { this.setState(State.CheckingForUpdates(false)); this.isUpdateAvailable().then(result => { if (result) { - this.setState(State.Ready({ version: 'something', productVersion: 'something' })); + this.setState(State.Ready({ version: 'something' })); } else { this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: false }); diff --git a/code/src/vs/platform/update/electron-main/updateService.win32.ts b/code/src/vs/platform/update/electron-main/updateService.win32.ts index ff8bbb0e559..4c49a758185 100644 --- a/code/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/code/src/vs/platform/update/electron-main/updateService.win32.ts @@ -135,7 +135,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); return this.cleanup(update.version).then(() => { return this.getUpdatePackagePath(update.version).then(updatePackagePath => { @@ -153,15 +153,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun .then(() => updatePackagePath); }); }).then(packagePath => { - const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); - this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); @@ -209,7 +207,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async doApplyUpdate(): Promise { - if (this.state.type !== StateType.Downloaded && this.state.type !== StateType.Downloading) { + if (this.state.type !== StateType.Downloaded) { return Promise.resolve(undefined); } @@ -273,14 +271,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); const update: IUpdate = { version: 'unknown', productVersion: 'unknown' }; - this.setState(State.Downloading(update)); + this.setState(State.Downloading); this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); diff --git a/code/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/code/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 6bd1d52b986..8386e94dab2 100644 --- a/code/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/code/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -212,7 +212,10 @@ export class UtilityProcess extends Disposable { const started = this.doStart(configuration); if (started && configuration.payload) { - this.postMessage(configuration.payload); + const posted = this.postMessage(configuration.payload); + if (posted) { + this.log('payload sent via postMessage()', Severity.Info); + } } return started; @@ -363,12 +366,14 @@ export class UtilityProcess extends Disposable { })); } - postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): void { + postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): boolean { if (!this.process) { - return; // already killed, crashed or never started + return false; // already killed, crashed or never started } this.process.postMessage(message, transfer); + + return true; } connect(payload?: unknown): Electron.MessagePortMain { diff --git a/code/src/vs/platform/windows/electron-main/windowImpl.ts b/code/src/vs/platform/windows/electron-main/windowImpl.ts index 807bb601136..e46474de06c 100644 --- a/code/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/code/src/vs/platform/windows/electron-main/windowImpl.ts @@ -975,6 +975,13 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + type appWithProxySupport = Electron.App & { + setProxy(config: Electron.Config): Promise; + resolveProxy(url: string): Promise; + }; + if (typeof (app as appWithProxySupport).setProxy === 'function') { + (app as appWithProxySupport).setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + } } } } diff --git a/code/src/vs/server/node/remoteTerminalChannel.ts b/code/src/vs/server/node/remoteTerminalChannel.ts index caec44f7f3a..657d3e8238a 100644 --- a/code/src/vs/server/node/remoteTerminalChannel.ts +++ b/code/src/vs/server/node/remoteTerminalChannel.ts @@ -32,6 +32,7 @@ import { IExtensionManagementService } from 'vs/platform/extensionManagement/com import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { promiseWithResolvers } from 'vs/base/common/async'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( @@ -235,7 +236,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< ); // Apply extension environment variable collections to the environment - if (!shellLaunchConfig.strictEnv) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const entries: [string, IEnvironmentVariableCollection][] = []; for (const [k, v, d] of args.envVariableCollections) { entries.push([k, { map: deserializeEnvironmentVariableCollection(v), descriptionMap: deserializeEnvironmentDescriptionMap(d) }]); diff --git a/code/src/vs/workbench/api/browser/extensionHost.contribution.ts b/code/src/vs/workbench/api/browser/extensionHost.contribution.ts index 2b59166259d..42ff6482c51 100644 --- a/code/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/code/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -84,7 +84,7 @@ import './mainThreadTimeline'; import './mainThreadTesting'; import './mainThreadSecretState'; import './mainThreadShare'; -import './mainThreadProfilContentHandlers'; +import './mainThreadProfileContentHandlers'; import './mainThreadAiRelatedInformation'; import './mainThreadAiEmbeddingVector'; import './mainThreadIssueReporter'; diff --git a/code/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/code/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 22c82811bb4..b3ebdd940c3 100644 --- a/code/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/code/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,18 +6,32 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { getAuthenticationProviderActivationEvent, addAccountUsage } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import type { AuthenticationGetSessionOptions } from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +interface AuthenticationForceNewSessionOptions { + detail?: string; + learnMore?: UriComponents; + sessionToRecreate?: AuthenticationSession; +} +interface AuthenticationGetSessionOptions { + clearSessionPreference?: boolean; + createIfNone?: boolean; + forceNewSession?: boolean | AuthenticationForceNewSessionOptions; + silent?: boolean; +} export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -58,11 +72,14 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu constructor( extHostContext: IExtHostContext, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, + @IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService, + @IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -100,23 +117,43 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu $removeSession(providerId: string, sessionId: string): Promise { return this.authenticationService.removeSession(providerId, sessionId); } - private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, detail?: string): Promise { + private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { const message = recreatingSession ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName) : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName); - const { confirmed } = await this.dialogService.confirm({ + + const buttons: IPromptButton[] = [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run() { + return true; + }, + } + ]; + if (options?.learnMore) { + buttons.push({ + label: nls.localize('learnMore', "Learn more"), + run: async () => { + const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); + return await result; + } + }); + } + const { result } = await this.dialogService.prompt({ type: Severity.Info, message, - detail, - primaryButton: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow") + buttons, + detail: options?.detail, + cancelButton: true, }); - return confirmed; + return result ?? false; } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { const sessions = await this.authenticationService.getSessions(providerId, scopes, true); - const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId); + const provider = this.authenticationService.getProvider(providerId); // Error cases if (options.forceNewSession && options.createIfNone) { @@ -131,22 +168,22 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { - if (supportsMultipleAccounts) { + if (provider.supportsMultipleAccounts) { if (options.clearSessionPreference) { // Clearing the session preference is usually paired with createIfNone, so just remove the preference and // defer to the rest of the logic in this function to choose the session. - this.authenticationService.removeSessionPreference(providerId, extensionId, scopes); + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); } else { // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); if (existingSessionPreference) { const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { return matchingSession; } } } - } else if (this.authenticationService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { + } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { return sessions[0]; } } @@ -154,51 +191,44 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We may need to prompt because we don't have a valid session // modal flows if (options.createIfNone || options.forceNewSession) { - const providerName = this.authenticationService.getLabel(providerId); - const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined; + let uiOptions: AuthenticationForceNewSessionOptions | undefined; + if (typeof options.forceNewSession === 'object') { + uiOptions = options.forceNewSession; + } // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(providerName, extensionName, recreatingSession, detail); + const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } let session; if (sessions?.length && !options.forceNewSession) { - session = supportsMultipleAccounts - ? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions) + session = provider.supportsMultipleAccounts + ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { let sessionToRecreate: AuthenticationSession | undefined; if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; } else { - const sessionIdToRecreate = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; } session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); } - this.authenticationService.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, session); + this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.authenticationExtensionsService.updateSessionPreference(providerId, extensionId, session); return session; } // For the silent flows, if we have a session, even though it may not be the user's preference, we'll return it anyway because it might be for a specific // set of scopes. - const validSession = sessions.find(session => this.authenticationService.isAccessAllowed(providerId, session.account.label, extensionId)); + const validSession = sessions.find(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId)); if (validSession) { - // Migration. If we have a valid session, but no preference, we'll set the preference to the valid session. - // TODO: Remove this after in a few releases. - if (!this.authenticationService.getSessionPreference(providerId, extensionId, scopes)) { - if (this.storageService.get(`${extensionName}-${providerId}`, StorageScope.APPLICATION)) { - this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.APPLICATION); - } - this.authenticationService.updateAllowedExtension(providerId, validSession.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, validSession); - } return validSession; } @@ -207,8 +237,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow, // otherwise request a new one. sessions.length - ? this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) - : await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); + ? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) + : await this.authenticationExtensionsService.requestNewSession(providerId, scopes, extensionId, extensionName); } return undefined; } @@ -218,7 +248,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu if (session) { this.sendProviderUsageTelemetry(extensionId, providerId); - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } return session; @@ -226,11 +256,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationService.isAccessAllowed(providerId, s.account.label, extensionId)); + const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); if (accessibleSessions.length) { this.sendProviderUsageTelemetry(extensionId, providerId); for (const session of accessibleSessions) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } } return accessibleSessions; diff --git a/code/src/vs/workbench/api/browser/mainThreadChat.ts b/code/src/vs/workbench/api/browser/mainThreadChat.ts index c7155e716a9..1a4d657cd94 100644 --- a/code/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/code/src/vs/workbench/api/browser/mainThreadChat.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostChatShape, ExtHostContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadChat) @@ -55,18 +55,10 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { return undefined; } - const responderAvatarIconUri = session.responderAvatarIconUri && - URI.revive(session.responderAvatarIconUri); - const emitter = new Emitter(); this._stateEmitters.set(session.id, emitter); return { id: session.id, - requesterUsername: session.requesterUsername, - requesterAvatarIconUri: URI.revive(session.requesterAvatarIconUri), - responderUsername: session.responderUsername, - responderAvatarIconUri, - inputPlaceholder: session.inputPlaceholder, dispose: () => { emitter.dispose(); this._stateEmitters.delete(session.id); @@ -83,13 +75,6 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { this._stateEmitters.get(sessionId)?.fire(state); } - async $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(providerId); - if (widget && widget.viewModel) { - this._chatService.sendRequestToProvider(widget.viewModel.sessionId, message); - } - } - async $unregisterChatProvider(handle: number): Promise { this._providerRegistrations.deleteAndDispose(handle); } diff --git a/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index c143be65078..631f51548dc 100644 --- a/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -19,8 +19,8 @@ import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionCh import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -29,7 +29,6 @@ import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/ext type AgentData = { dispose: () => void; name: string; - hasSlashCommands?: boolean; hasFollowups?: boolean; }; @@ -49,6 +48,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatContributionService private readonly _chatContributionService: IChatContributionService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -76,12 +76,13 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.deleteAndDispose(handle); } - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void { - const lastSlashCommands: WeakMap = new WeakMap(); - const d = this._chatAgentService.registerAgent({ - id: name, - extensionId: extension, - metadata: revive(metadata), + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void { + const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name); + if (!staticAgentRegistration && !allowDynamic) { + throw new Error(`chatParticipant must be declared in package.json: ${name}`); + } + + const impl: IChatAgentImplementation = { invoke: async (request, progress, history, token) => { this._pendingProgress.set(request.requestId, progress); try { @@ -90,26 +91,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._pendingProgress.delete(request.requestId); } }, - provideFollowups: async (request, result, token): Promise => { + provideFollowups: async (request, result, history, token): Promise => { if (!this._agents.get(handle)?.hasFollowups) { return []; } - return this._proxy.$provideFollowups(request, handle, result, token); - }, - getLastSlashCommands: (model: IChatModel) => { - return lastSlashCommands.get(model); - }, - provideSlashCommands: async (model, history, token) => { - if (!this._agents.get(handle)?.hasSlashCommands) { - return []; // save an IPC call - } - const commands = await this._proxy.$provideSlashCommands(handle, { history }, token); - if (model) { - lastSlashCommands.set(model, commands); - } - - return commands; + return this._proxy.$provideFollowups(request, handle, result, { history }, token); }, provideWelcomeMessage: (token: CancellationToken) => { return this._proxy.$provideWelcomeMessage(handle, token); @@ -117,11 +104,26 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA provideSampleQuestions: (token: CancellationToken) => { return this._proxy.$provideSampleQuestions(handle, token); } - }); + }; + + let disposable: IDisposable; + if (!staticAgentRegistration && allowDynamic) { + disposable = this._chatAgentService.registerDynamicAgent( + { + id: name, + extensionId: extension, + metadata: revive(metadata), + slashCommands: [], + locations: [ChatAgentLocation.Panel] // TODO all dynamic participants are panel only? + }, + impl); + } else { + disposable = this._chatAgentService.registerAgent(name, impl); + } + this._agents.set(handle, { name, - dispose: d.dispose, - hasSlashCommands: metadata.hasSlashCommands, + dispose: disposable.dispose, hasFollowups: metadata.hasFollowups }); } @@ -131,7 +133,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA if (!data) { throw new Error(`No agent with handle ${handle} registered`); } - data.hasSlashCommands = metadataUpdate.hasSlashCommands; data.hasFollowups = metadataUpdate.hasFollowups; this._chatAgentService.updateAgent(data.name, revive(metadataUpdate)); } diff --git a/code/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/code/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index 4ad9e654807..3fa0eef969f 100644 --- a/code/src/vs/workbench/api/browser/mainThreadCodeInsets.ts +++ b/code/src/vs/workbench/api/browser/mainThreadCodeInsets.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getWindow } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -43,7 +44,7 @@ class EditorWebviewZone implements IViewZone { this.heightInLines = height; editor.changeViewZones(accessor => this._id = accessor.addZone(this)); - webview.mountTo(this.domNode); + webview.mountTo(this.domNode, getWindow(editor.getDomNode())); } dispose(): void { diff --git a/code/src/vs/workbench/api/browser/mainThreadComments.ts b/code/src/vs/workbench/api/browser/mainThreadComments.ts index 538c754e1b3..5bfadbbc409 100644 --- a/code/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/code/src/vs/workbench/api/browser/mainThreadComments.ts @@ -26,6 +26,7 @@ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { Schemas } from 'vs/base/common/network'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; export class MainThreadCommentThread implements languages.CommentThread { private _input?: languages.CommentInput; @@ -150,6 +151,20 @@ export class MainThreadCommentThread implements languages.CommentThread { this._onDidChangeState.fire(this._state); } + private _applicability: languages.CommentThreadApplicability | undefined; + + get applicability(): languages.CommentThreadApplicability | undefined { + return this._applicability; + } + + set applicability(value: languages.CommentThreadApplicability | undefined) { + this._applicability = value; + this._onDidChangeApplicability.fire(value); + } + + private readonly _onDidChangeApplicability = new Emitter(); + readonly onDidChangeApplicability: Event = this._onDidChangeApplicability.event; + public get isTemplate(): boolean { return this._isTemplate; } @@ -184,6 +199,7 @@ export class MainThreadCommentThread implements languages.CommentThread { if (modified('collapseState')) { this.initialCollapsibleState = changes.collapseState; } if (modified('canReply')) { this.canReply = changes.canReply!; } if (modified('state')) { this.state = changes.state!; } + if (modified('applicability')) { this.applicability = changes.applicability!; } if (modified('isTemplate')) { this._isTemplate = changes.isTemplate!; } } @@ -197,7 +213,7 @@ export class MainThreadCommentThread implements languages.CommentThread { this._onDidChangeState.dispose(); } - toJSON(): any { + toJSON(): MarshalledCommentThread { return { $mid: MarshalledId.CommentThread, commentControlHandle: this.controllerHandle, @@ -248,6 +264,10 @@ export class MainThreadCommentController implements ICommentController { return this._features; } + get owner() { + return this._id; + } + constructor( private readonly _proxy: ExtHostCommentsShape, private readonly _commentService: ICommentService, @@ -370,8 +390,8 @@ export class MainThreadCommentController implements ICommentController { } } - updateCommentingRanges() { - this._commentService.updateCommentingRanges(this._uniqueId); + updateCommentingRanges(resourceHints?: languages.CommentingRangeResourceHint) { + this._commentService.updateCommentingRanges(this._uniqueId, resourceHints); } private getKnownThread(commentThreadHandle: number): MainThreadCommentThread { @@ -385,7 +405,7 @@ export class MainThreadCommentController implements ICommentController { async getDocumentComments(resource: URI, token: CancellationToken) { if (resource.scheme === Schemas.vscodeNotebookCell) { return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: [], commentingRanges: { @@ -407,7 +427,7 @@ export class MainThreadCommentController implements ICommentController { const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: ret, commentingRanges: { @@ -421,7 +441,7 @@ export class MainThreadCommentController implements ICommentController { async getNotebookComments(resource: URI, token: CancellationToken) { if (resource.scheme !== Schemas.vscodeNotebookCell) { return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: [] }; @@ -436,7 +456,7 @@ export class MainThreadCommentController implements ICommentController { } return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: ret }; @@ -591,14 +611,14 @@ export class MainThreadComments extends Disposable implements MainThreadComments return provider.deleteCommentThread(commentThreadHandle); } - $updateCommentingRanges(handle: number) { + $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint) { const provider = this._commentControllers.get(handle); if (!provider) { return; } - provider.updateCommentingRanges(); + provider.updateCommentingRanges(resourceHints); } private registerView(commentsViewAlreadyRegistered: boolean) { diff --git a/code/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/code/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index d32e4ef318e..3f992a4cfad 100644 --- a/code/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/code/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -664,6 +664,8 @@ class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustom } } + public get canHotExit() { return typeof this._backupId === 'string' && this._hotExitState.type === HotExitState.Type.Allowed; } + public async backup(token: CancellationToken): Promise { const editors = this._getEditors(); if (!editors.length) { @@ -735,6 +737,6 @@ class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustom return backupData; } - throw new Error(`Cannot back up in this state: ${errorMessage}`); + throw new Error(`Cannot backup in this state: ${errorMessage}`); } } diff --git a/code/src/vs/workbench/api/browser/mainThreadDebugService.ts b/code/src/vs/workbench/api/browser/mainThreadDebugService.ts index 3178df6d093..e5dfc1a814e 100644 --- a/code/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/code/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -5,7 +5,7 @@ import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI as uri, UriComponents } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto @@ -18,6 +18,7 @@ import { convertToVSCPaths, convertToDAPaths, isSessionAttach } from 'vs/workben import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { Event } from 'vs/base/common/event'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -88,28 +89,28 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb this._debugAdapterDescriptorFactories = new Map(); this._extHostKnownSessions = new Set(); - this._toDispose.add(this.debugService.getViewModel().onDidFocusThread(({ thread, explicit, session }) => { - if (session) { - const dto: IThreadFocusDto = { + const viewModel = this.debugService.getViewModel(); + this._toDispose.add(Event.any(viewModel.onDidFocusStackFrame, viewModel.onDidFocusThread)(() => { + const stackFrame = viewModel.focusedStackFrame; + const thread = viewModel.focusedThread; + if (stackFrame) { + this._proxy.$acceptStackFrameFocus({ + kind: 'stackFrame', + threadId: stackFrame.thread.threadId, + frameId: stackFrame.frameId, + sessionId: stackFrame.thread.session.getId(), + } satisfies IStackFrameFocusDto); + } else if (thread) { + this._proxy.$acceptStackFrameFocus({ kind: 'thread', - threadId: thread?.threadId, - sessionId: session.getId(), - }; - this._proxy.$acceptStackFrameFocus(dto); + threadId: thread.threadId, + sessionId: thread.session.getId(), + } satisfies IThreadFocusDto); + } else { + this._proxy.$acceptStackFrameFocus(undefined); } })); - this._toDispose.add(this.debugService.getViewModel().onDidFocusStackFrame(({ stackFrame, explicit, session }) => { - if (session) { - const dto: IStackFrameFocusDto = { - kind: 'stackFrame', - threadId: stackFrame?.thread.threadId, - frameId: stackFrame?.frameId, - sessionId: session.getId(), - }; - this._proxy.$acceptStackFrameFocus(dto); - } - })); this.sendBreakpointsAndListen(); } @@ -225,7 +226,14 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } else if (dto.type === 'function') { this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); } else if (dto.type === 'data') { - this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist, dto.accessTypes, dto.accessType, dto.mode); + this.debugService.addDataBreakpoint({ + description: dto.label, + src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId }, + canPersist: dto.canPersist, + accessTypes: dto.accessTypes, + accessType: dto.accessType, + mode: dto.mode + }); } } return Promise.resolve(); @@ -436,19 +444,20 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb logMessage: fbp.logMessage, functionName: fbp.name }; - } else if ('dataId' in bp) { + } else if ('src' in bp) { const dbp = bp; - return { + return { type: 'data', id: dbp.getId(), - dataId: dbp.dataId, + dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address, enabled: dbp.enabled, condition: dbp.condition, hitCondition: dbp.hitCondition, logMessage: dbp.logMessage, + accessType: dbp.accessType, label: dbp.description, canPersist: dbp.canPersist - }; + } satisfies IDataBreakpointDto; } else { const sbp = bp; return { diff --git a/code/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/code/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index d23f8e91fd5..e0f966a9fb2 100644 --- a/code/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/code/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, AnyInputDto, TabInputKind, TabModelOperationKind } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, AnyInputDto, TabInputKind, TabModelOperationKind, TextDiffInputDto } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { EditorResourceAccessor, GroupModelChangeKind, SideBySideEditor } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -26,6 +26,7 @@ import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { ILogService } from 'vs/platform/log/common/log'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; interface TabInfo { tab: IEditorTabDto; @@ -199,6 +200,24 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { }; } + if (editor instanceof MultiDiffEditorInput) { + const diffEditors: TextDiffInputDto[] = []; + for (const resource of (editor?.initialResources ?? [])) { + if (resource.original && resource.modified) { + diffEditors.push({ + kind: TabInputKind.TextDiffInput, + original: resource.original, + modified: resource.modified + }); + } + } + + return { + kind: TabInputKind.MultiDiffEditorInput, + diffEditors + }; + } + return { kind: TabInputKind.UnknownInput }; } @@ -553,6 +572,9 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { this._onDidTabPreviewChange(groupId, event.editorIndex, event.editor); break; } + case GroupModelChangeKind.EDITOR_TRANSIENT: + // Currently not exposed in the API + break; case GroupModelChangeKind.EDITOR_MOVE: if (isGroupEditorMoveEvent(event) && event.editor && event.editorIndex !== undefined && event.oldEditorIndex !== undefined) { this._onDidTabMove(groupId, event.editorIndex, event.oldEditorIndex, event.editor); diff --git a/code/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/code/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 20c6c76910d..22976049d5a 100644 --- a/code/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/code/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -230,7 +230,7 @@ export class MainThreadFileSystemEventService implements MainThreadFileSystemEve opts.recursive = false; } } catch (error) { - this._logService.error(`MainThreadFileSystemEventService#$watch(): failed to stat a resource for file watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}): ${error}`); + // ignore } } @@ -254,21 +254,9 @@ export class MainThreadFileSystemEventService implements MainThreadFileSystemEve // Uncorrelated file watching gets special treatment else { + this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching uncorrelated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); - // Refuse to watch anything that is already watched via - // our workspace watchers in case the request is a - // recursive file watcher and does not opt-in to event - // correlation via specific exclude rules. - // Still allow for non-recursive watch requests as a way - // to bypass configured exclude rules though - // (see https://github.com/microsoft/vscode/issues/146066) const workspaceFolder = this._contextService.getWorkspaceFolder(uri); - if (workspaceFolder && opts.recursive) { - this._logService.trace(`MainThreadFileSystemEventService#$watch(): ignoring request to start watching because path is inside workspace (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); - return; - } - - this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching uncorrelated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); // Automatically add `files.watcherExclude` patterns when watching // recursively to give users a chance to configure exclude rules @@ -295,7 +283,7 @@ export class MainThreadFileSystemEventService implements MainThreadFileSystemEve // `/bar` but will not work as include for files within // `bar` unless a suffix of `/**` if added. // (https://github.com/microsoft/vscode/issues/148245) - else if (workspaceFolder) { + else if (!opts.recursive && workspaceFolder) { const config = this._configurationService.getValue(); if (config.files?.watcherExclude) { for (const key in config.files.watcherExclude) { diff --git a/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 80d13f11362..d89eecf9a3f 100644 --- a/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -32,9 +32,10 @@ import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy' import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; import { ResourceMap } from 'vs/base/common/map'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -398,8 +399,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread private readonly _pasteEditProviders = new Map(); - $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], id: string, metadata: IPasteEditProviderMetadataDto): void { - const provider = new MainThreadPasteEditProvider(handle, this._proxy, id, metadata, this._uriIdentService); + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IPasteEditProviderMetadataDto): void { + const provider = new MainThreadPasteEditProvider(handle, this._proxy, metadata, this._uriIdentService); this._pasteEditProviders.set(handle, provider); this._registrations.set(handle, combinedDisposable( this._languageFeaturesService.documentPasteEditProvider.register(selector, provider), @@ -601,9 +602,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); } }, - handlePartialAccept: async (completions, item, acceptedCharacters): Promise => { + handlePartialAccept: async (completions, item, acceptedCharacters, info: languages.PartialAcceptInfo): Promise => { if (supportsHandleEvents) { - await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters); + await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters, info); } }, freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { @@ -963,8 +964,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread private readonly _documentOnDropEditProviders = new Map(); - $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], id: string | undefined, metadata: IDocumentDropEditProviderMetadata): void { - const provider = new MainThreadDocumentOnDropEditProvider(handle, this._proxy, id, metadata, this._uriIdentService); + $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IDocumentDropEditProviderMetadata): void { + const provider = new MainThreadDocumentOnDropEditProvider(handle, this._proxy, metadata, this._uriIdentService); this._documentOnDropEditProviders.set(handle, provider); this._registrations.set(handle, combinedDisposable( this._languageFeaturesService.documentOnDropEditProvider.register(selector, provider), @@ -992,23 +993,23 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider private readonly dataTransfers = new DataTransferFileCache(); - public readonly id: string; public readonly copyMimeTypes?: readonly string[]; public readonly pasteMimeTypes?: readonly string[]; + public readonly providedPasteEditKinds?: readonly HierarchicalKind[]; readonly prepareDocumentPaste?: languages.DocumentPasteEditProvider['prepareDocumentPaste']; readonly provideDocumentPasteEdits?: languages.DocumentPasteEditProvider['provideDocumentPasteEdits']; + readonly resolveDocumentPasteEdit?: languages.DocumentPasteEditProvider['resolveDocumentPasteEdit']; constructor( private readonly _handle: number, private readonly _proxy: ExtHostLanguageFeaturesShape, - id: string, metadata: IPasteEditProviderMetadataDto, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService ) { - this.id = id; this.copyMimeTypes = metadata.copyMimeTypes; this.pasteMimeTypes = metadata.pasteMimeTypes; + this.providedPasteEditKinds = metadata.providedPasteEditKinds?.map(kind => new HierarchicalKind(kind)); if (metadata.supportsCopy) { this.prepareDocumentPaste = async (model: ITextModel, selections: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise => { @@ -1039,20 +1040,41 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider return; } - const result = await this._proxy.$providePasteEdits(this._handle, request.id, model.uri, selections, dataTransferDto, token); - if (!result) { + const edits = await this._proxy.$providePasteEdits(this._handle, request.id, model.uri, selections, dataTransferDto, { + only: context.only?.value, + triggerKind: context.triggerKind, + }, token); + if (!edits) { return; } return { - ...result, - additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined, + edits: edits.map((edit): languages.DocumentPasteEdit => { + return { + ...edit, + kind: edit.kind ? new HierarchicalKind(edit.kind.value) : new HierarchicalKind(''), + yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), + additionalEdit: edit.additionalEdit ? reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined, + }; + }), + dispose: () => { + this._proxy.$releasePasteEdits(this._handle, request.id); + }, }; } finally { request.dispose(); } }; } + if (metadata.supportsResolve) { + this.resolveDocumentPasteEdit = async (edit: languages.DocumentPasteEdit, token: CancellationToken) => { + const resolved = await this._proxy.$resolvePasteEdit(this._handle, (edit)._cacheId!, token); + if (resolved.additionalEdit) { + edit.additionalEdit = reviveWorkspaceEditDto(resolved.additionalEdit, this._uriIdentService); + } + return edit; + }; + } } resolveFileData(requestId: number, dataId: string): Promise { @@ -1064,21 +1086,18 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd private readonly dataTransfers = new DataTransferFileCache(); - readonly id: string | undefined; readonly dropMimeTypes?: readonly string[]; constructor( private readonly _handle: number, private readonly _proxy: ExtHostLanguageFeaturesShape, - id: string | undefined, metadata: IDocumentDropEditProviderMetadata | undefined, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService ) { - this.id = id; this.dropMimeTypes = metadata?.dropMimeTypes ?? ['*/*']; } - async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const request = this.dataTransfers.add(dataTransfer); try { const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer); @@ -1086,15 +1105,19 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd return; } - const edit = await this._proxy.$provideDocumentOnDropEdits(this._handle, request.id, model.uri, position, dataTransferDto, token); - if (!edit) { + const edits = await this._proxy.$provideDocumentOnDropEdits(this._handle, request.id, model.uri, position, dataTransferDto, token); + if (!edits) { return; } - return { - ...edit, - additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), - }; + return edits.map(edit => { + return { + ...edit, + yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), + kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, + additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), + }; + }); } finally { request.dispose(); } diff --git a/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 6e33d7da99d..9a886a6713a 100644 --- a/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -13,6 +14,7 @@ import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -32,11 +34,12 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, @ILogService private readonly _logService: ILogService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService, @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); - this._proxy.$updateLanguageModels({ added: _chatProviderService.getLanguageModelIds() }); + this._proxy.$updateLanguageModels({ added: coalesce(_chatProviderService.getLanguageModelIds().map(id => _chatProviderService.lookupLanguageModel(id))) }); this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$updateLanguageModels, this._proxy)); } @@ -64,7 +67,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { } dipsosables.add(Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ id: `lm-${identifier}`, - label: localize('languageModels', "Language Model ({0})", `${identifier}-${metadata.model}`), + label: localize('languageModels', "Language Model ({0})", `${identifier}`), access: { canToggle: false, }, @@ -91,7 +94,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { await Promise.race([ activate, - Event.toPromise(Event.filter(this._chatProviderService.onDidChangeLanguageModels, e => Boolean(e.added?.includes(providerId)))) + Event.toPromise(Event.filter(this._chatProviderService.onDidChangeLanguageModels, e => Boolean(e.added?.some(value => value.identifier === providerId)))) ]); return this._chatProviderService.lookupLanguageModel(providerId); @@ -131,28 +134,8 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { disposables.add(toDisposable(() => { this._authenticationService.unregisterAuthenticationProvider(authProviderId); })); - disposables.add(this._authenticationService.onDidChangeSessions(async (e) => { - if (e.providerId === authProviderId) { - if (e.event.removed?.length) { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); - const extensionsToUpdateAccess = []; - for (const allowed of allowedExtensions) { - const from = await this._extensionService.getExtension(allowed.id); - this._authenticationService.updateAllowedExtension(authProviderId, authProviderId, allowed.id, allowed.name, false); - if (from) { - extensionsToUpdateAccess.push({ - from: from.identifier, - to: extension, - enabled: false - }); - } - } - this._proxy.$updateModelAccesslist(extensionsToUpdateAccess); - } - } - })); - disposables.add(this._authenticationService.onDidChangeExtensionSessionAccess(async (e) => { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); + disposables.add(this._authenticationAccessService.onDidChangeExtensionSessionAccess(async (e) => { + const allowedExtensions = this._authenticationAccessService.readAllowedExtensions(authProviderId, accountLabel); const accessList = []; for (const allowedExtension of allowedExtensions) { const from = await this._extensionService.getExtension(allowedExtension.id); diff --git a/code/src/vs/workbench/api/browser/mainThreadProfilContentHandlers.ts b/code/src/vs/workbench/api/browser/mainThreadProfileContentHandlers.ts similarity index 100% rename from code/src/vs/workbench/api/browser/mainThreadProfilContentHandlers.ts rename to code/src/vs/workbench/api/browser/mainThreadProfileContentHandlers.ts diff --git a/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index 48f53faea39..d5312097ee9 100644 --- a/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts @@ -3,7 +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 { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostContext, ExtHostQuickDiffShape, IDocumentFilterDto, MainContext, MainThreadQuickDiffShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -30,7 +30,7 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { selector, isSCM: false, getOriginalResource: async (uri: URI) => { - return URI.revive(await this.proxy.$provideOriginalResource(handle, uri, new CancellationTokenSource().token)); + return URI.revive(await this.proxy.$provideOriginalResource(handle, uri, CancellationToken.None)); } }; const disposable = this.quickDiffService.addQuickDiffProvider(provider); diff --git a/code/src/vs/workbench/api/browser/mainThreadSearch.ts b/code/src/vs/workbench/api/browser/mainThreadSearch.ts index 797a4ee92a4..9b1af87b574 100644 --- a/code/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/code/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -9,9 +9,11 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IFileMatch, IFileQuery, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISearchProgressItem, ISearchResultProvider, ISearchService, ITextQuery, QueryType, SearchProviderType } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IFileQuery, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, ITextQuery, QueryType, SearchProviderType } from 'vs/workbench/services/search/common/search'; import { ExtHostContext, ExtHostSearchShape, MainContext, MainThreadSearchShape } from '../common/extHost.protocol'; import { revive } from 'vs/base/common/marshalling'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @extHostNamedCustomer(MainContext.MainThreadSearch) export class MainThreadSearch implements MainThreadSearchShape { @@ -24,6 +26,7 @@ export class MainThreadSearch implements MainThreadSearchShape { @ISearchService private readonly _searchService: ISearchService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IConfigurationService _configurationService: IConfigurationService, + @IContextKeyService protected contextKeyService: IContextKeyService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSearch); this._proxy.$enableExtensionHostSearch(); @@ -38,6 +41,11 @@ export class MainThreadSearch implements MainThreadSearchShape { this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.text, scheme, handle, this._proxy)); } + $registerAITextSearchProvider(handle: number, scheme: string): void { + Constants.SearchContext.hasAIResultProvider.bindTo(this.contextKeyService).set(true); + this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.aiText, scheme, handle, this._proxy)); + } + $registerFileSearchProvider(handle: number, scheme: string): void { this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.file, scheme, handle, this._proxy)); } @@ -64,7 +72,6 @@ export class MainThreadSearch implements MainThreadSearchShape { provider.handleFindMatch(session, data); } - $handleTelemetry(eventName: string, data: any): void { this._telemetryService.publicLog(eventName, data); } @@ -126,7 +133,7 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { return this.doSearch(query, onProgress, token); } - doSearch(query: ITextQuery | IFileQuery, onProgress?: (p: ISearchProgressItem) => void, token: CancellationToken = CancellationToken.None): Promise { + doSearch(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void, token: CancellationToken = CancellationToken.None): Promise { if (!query.folderQueries.length) { throw new Error('Empty folderQueries'); } @@ -134,9 +141,7 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { const search = new SearchOperation(onProgress); this._searches.set(search.id, search); - const searchP = query.type === QueryType.File - ? this._proxy.$provideFileSearchResults(this._handle, search.id, query, token) - : this._proxy.$provideTextSearchResults(this._handle, search.id, query, token); + const searchP = this._provideSearchResults(query, search.id, token); return Promise.resolve(searchP).then((result: ISearchCompleteStats) => { this._searches.delete(search.id); @@ -169,4 +174,15 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { } }); } + + private _provideSearchResults(query: ISearchQuery, session: number, token: CancellationToken): Promise { + switch (query.type) { + case QueryType.File: + return this._proxy.$provideFileSearchResults(this._handle, session, query, token); + case QueryType.Text: + return this._proxy.$provideTextSearchResults(this._handle, session, query, token); + default: + return this._proxy.$provideAITextSearchResults(this._handle, session, query, token); + } + } } diff --git a/code/src/vs/workbench/api/browser/mainThreadTesting.ts b/code/src/vs/workbench/api/browser/mainThreadTesting.ts index fa5376ca4b1..c9976ce3727 100644 --- a/code/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/code/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -7,7 +7,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ISettableObservable } from 'vs/base/common/observable'; +import { ISettableObservable, transaction } from 'vs/base/common/observable'; import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; @@ -58,10 +58,17 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh })); this._register(resultService.onResultsChanged(evt => { - const results = 'completed' in evt ? evt.completed : ('inserted' in evt ? evt.inserted : undefined); - const serialized = results?.toJSONWithMessages(); - if (serialized) { - this.proxy.$publishTestResults([serialized]); + if ('completed' in evt) { + const serialized = evt.completed.toJSONWithMessages(); + if (serialized) { + this.proxy.$publishTestResults([serialized]); + } + } else if ('removed' in evt) { + evt.removed.forEach(r => { + if (r instanceof LiveTestResult) { + this.proxy.$disposeRun(r.id); + } + }); } })); } @@ -121,21 +128,28 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - $signalCoverageAvailable(runId: string, taskId: string, available: boolean): void { + $appendCoverage(runId: string, taskId: string, coverage: IFileCoverage.Serialized): void { this.withLiveRun(runId, run => { const task = run.tasks.find(t => t.id === taskId); if (!task) { return; } - const fn = available ? ((token: CancellationToken) => TestCoverage.load(taskId, { - provideFileCoverage: async token => await this.proxy.$provideFileCoverage(runId, taskId, token) - .then(c => c.map(u => IFileCoverage.deserialize(this.uriIdentityService, u))), - resolveFileCoverage: (i, token) => this.proxy.$resolveFileCoverage(runId, taskId, i, token) - .then(d => d.map(CoverageDetails.deserialize)), - }, this.uriIdentityService, token)) : undefined; - - (task.coverage as ISettableObservable Promise)>).set(fn, undefined); + const deserialized = IFileCoverage.deserialize(this.uriIdentityService, coverage); + + transaction(tx => { + let value = task.coverage.read(undefined); + if (!value) { + value = new TestCoverage(taskId, this.uriIdentityService, { + getCoverageDetails: (id, token) => this.proxy.$getCoverageDetails(id, token) + .then(r => r.map(CoverageDetails.deserialize)), + }); + value.append(deserialized, tx); + (task.coverage as ISettableObservable).set(value, tx); + } else { + value.append(deserialized, tx); + } + }); }); } diff --git a/code/src/vs/workbench/api/common/extHost.api.impl.ts b/code/src/vs/workbench/api/common/extHost.api.impl.ts index 3bf78d8ed24..ef04f92dc12 100644 --- a/code/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/code/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable } from 'vs/base/common/lifecycle'; @@ -25,11 +25,11 @@ import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, MainC import { ExtHostRelatedInformation } from 'vs/workbench/api/common/extHostAiRelatedInformation'; import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; +import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; import { ExtHostChat } from 'vs/workbench/api/common/extHostChat'; import { ExtHostChatAgents2 } from 'vs/workbench/api/common/extHostChatAgents2'; -import { ExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; +import { IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { ExtHostChatVariables } from 'vs/workbench/api/common/extHostChatVariables'; import { ExtHostClipboard } from 'vs/workbench/api/common/extHostClipboard'; import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets'; @@ -143,6 +143,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostSecretState = accessor.get(IExtHostSecretState); const extHostEditorTabs = accessor.get(IExtHostEditorTabs); const extHostManagedSockets = accessor.get(IExtHostManagedSockets); + const extHostAuthentication = accessor.get(IExtHostAuthentication); + const extHostLanguageModels = accessor.get(IExtHostLanguageModels); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -157,6 +159,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostTelemetry, extHostTelemetry); rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, extHostEditorTabs); rpcProtocol.set(ExtHostContext.ExtHostManagedSockets, extHostManagedSockets); + rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication); + rpcProtocol.set(ExtHostContext.ExtHostChatProvider, extHostLanguageModels); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -196,7 +200,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); - const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.remote, extHostWorkspace, extHostLogService, extHostApiDeprecation)); const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); @@ -207,7 +210,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInlineChat, new ExtHostInteractiveEditor(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService)); - const extHostChatProvider = rpcProtocol.set(ExtHostContext.ExtHostChatProvider, new ExtHostLanguageModels(rpcProtocol, extHostLogService, extHostAuthentication)); const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol)); @@ -284,6 +286,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const authentication: typeof vscode.authentication = { getSession(providerId: string, scopes: readonly string[], options?: vscode.AuthenticationGetSessionOptions) { + if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { + checkProposedApiEnabled(extension, 'authLearnMore'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getSessions(providerId: string, scopes: readonly string[]) { @@ -536,7 +541,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const interalSelector = typeConverters.LanguageSelector.from(selector); let notebook: vscode.NotebookDocument | undefined; if (targetsNotebooks(interalSelector)) { - notebook = extHostNotebook.notebookDocuments.find(value => Boolean(value.getCell(document.uri)))?.apiNotebook; + notebook = extHostNotebook.notebookDocuments.find(value => value.apiNotebook.getCells().find(c => c.document === document))?.apiNotebook; } return score(interalSelector, document.uri, document.languageId, true, notebook?.uri, notebook?.notebookType); }, @@ -1112,6 +1117,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'textSearchProvider'); return extHostSearch.registerTextSearchProvider(scheme, provider); }, + registerAITextSearchProvider: (scheme: string, provider: vscode.AITextSearchProvider) => { + // there are some dependencies on textSearchProvider, so we need to check for both + checkProposedApiEnabled(extension, 'aiTextSearchProvider'); + checkProposedApiEnabled(extension, 'textSearchProvider'); + return extHostSearch.registerAITextSearchProvider(scheme, provider); + }, registerRemoteAuthorityResolver: (authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver) => { checkProposedApiEnabled(extension, 'resolvers'); return extensionService.registerRemoteAuthorityResolver(authorityPrefix, resolver); @@ -1230,8 +1241,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get breakpoints() { return extHostDebugService.breakpoints; }, - get stackFrameFocus() { - return extHostDebugService.stackFrameFocus; + get activeStackItem() { + if (!isProposedApiEnabled(extension, 'debugFocus')) { + return undefined; + } + return extHostDebugService.activeStackItem; }, registerDebugVisualizationProvider(id, provider) { checkProposedApiEnabled(extension, 'debugVisualization'); @@ -1256,9 +1270,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidChangeBreakpoints(listener, thisArgs?, disposables?) { return _asExtensionEvent(extHostDebugService.onDidChangeBreakpoints)(listener, thisArgs, disposables); }, - onDidChangeStackFrameFocus(listener, thisArg?, disposables?) { + onDidChangeActiveStackItem(listener, thisArg?, disposables?) { checkProposedApiEnabled(extension, 'debugFocus'); - return _asExtensionEvent(extHostDebugService.onDidChangeStackFrameFocus)(listener, thisArg, disposables); + return _asExtensionEvent(extHostDebugService.onDidChangeActiveStackItem)(listener, thisArg, disposables); }, registerDebugConfigurationProvider(debugType: string, provider: vscode.DebugConfigurationProvider, triggerKind?: vscode.DebugConfigurationProviderTriggerKind) { return extHostDebugService.registerDebugConfigurationProvider(debugType, provider, triggerKind || DebugConfigurationProviderTriggerKind.Initial); @@ -1377,10 +1391,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'interactive'); return extHostChat.registerChatProvider(extension, id, provider); }, - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest) { - checkProposedApiEnabled(extension, 'interactive'); - return extHostChat.sendInteractiveRequestToProvider(providerId, message); - }, transferChatSession(session: vscode.InteractiveSession, toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); return extHostChat.transferChatSession(session, toWorkspace); @@ -1407,7 +1417,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const chat: typeof vscode.chat = { registerChatResponseProvider(id: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata) { checkProposedApiEnabled(extension, 'chatProvider'); - return extHostChatProvider.registerLanguageModel(extension, id, provider, metadata); + return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); }, registerChatVariableResolver(name: string, description: string, resolver: vscode.ChatVariableResolver) { checkProposedApiEnabled(extension, 'chatVariableResolver'); @@ -1425,31 +1435,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: lm const lm: typeof vscode.lm = { - requestLanguageModelAccess(id, options) { - checkProposedApiEnabled(extension, 'languageModels'); - return extHostChatProvider.requestLanguageModelAccess(extension, id, options); - }, get languageModels() { checkProposedApiEnabled(extension, 'languageModels'); - return extHostChatProvider.getLanguageModelIds(); + return extHostLanguageModels.getLanguageModelIds(); }, onDidChangeLanguageModels: (listener, thisArgs?, disposables?) => { checkProposedApiEnabled(extension, 'languageModels'); - return extHostChatProvider.onDidChangeProviders(listener, thisArgs, disposables); + return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, - makeChatRequest(languageModel: string, messages: vscode.LanguageModelMessage[], optionsOrToken: { [name: string]: any } | vscode.CancellationToken, token?: vscode.CancellationToken) { + sendChatRequest(languageModel: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: vscode.CancellationToken) { checkProposedApiEnabled(extension, 'languageModels'); - let options: Record; - if (CancellationToken.isCancellationToken(optionsOrToken)) { - options = {}; - token = optionsOrToken; - } else if (CancellationToken.isCancellationToken(token)) { - options = optionsOrToken; - token = token; - } else { - throw new Error('Invalid arguments'); - } - return extHostChatProvider.makeChatRequest(extension, languageModel, messages, options, token); + return extHostLanguageModels.sendChatRequest(extension, languageModel, messages, options, token); } }; @@ -1507,6 +1503,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CommentState: extHostTypes.CommentState, CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, CommentThreadState: extHostTypes.CommentThreadState, + CommentThreadApplicability: extHostTypes.CommentThreadApplicability, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionItemTag: extHostTypes.CompletionItemTag, @@ -1617,7 +1614,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ViewColumn: extHostTypes.ViewColumn, WorkspaceEdit: extHostTypes.WorkspaceEdit, // proposed api types + DocumentPasteTriggerKind: extHostTypes.DocumentPasteTriggerKind, DocumentDropEdit: extHostTypes.DocumentDropEdit, + DocumentPasteEditKind: extHostTypes.DocumentPasteEditKind, DocumentPasteEdit: extHostTypes.DocumentPasteEdit, InlayHint: extHostTypes.InlayHint, InlayHintLabelPart: extHostTypes.InlayHintLabelPart, @@ -1674,16 +1673,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TabInputTerminal: extHostTypes.TerminalEditorTabInput, TabInputInteractiveWindow: extHostTypes.InteractiveWindowInput, TabInputChat: extHostTypes.ChatEditorTabInput, + TabInputTextMultiDiff: extHostTypes.TextMultiDiffTabInput, TelemetryTrustedValue: TelemetryTrustedValue, LogLevel: LogLevel, EditSessionIdentityMatch: EditSessionIdentityMatch, InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, ChatCopyKind: extHostTypes.ChatCopyKind, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, - StackFrameFocus: extHostTypes.StackFrameFocus, - ThreadFocus: extHostTypes.ThreadFocus, + StackFrame: extHostTypes.StackFrame, + Thread: extHostTypes.Thread, RelatedInformationType: extHostTypes.RelatedInformationType, SpeechToTextStatus: extHostTypes.SpeechToTextStatus, + PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, @@ -1693,9 +1694,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, - LanguageModelSystemMessage: extHostTypes.LanguageModelSystemMessage, - LanguageModelUserMessage: extHostTypes.LanguageModelUserMessage, - LanguageModelAssistantMessage: extHostTypes.LanguageModelAssistantMessage, + ChatLocation: extHostTypes.ChatLocation, + LanguageModelChatSystemMessage: extHostTypes.LanguageModelChatSystemMessage, + LanguageModelChatUserMessage: extHostTypes.LanguageModelChatUserMessage, + LanguageModelChatAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage, + LanguageModelSystemMessage: extHostTypes.LanguageModelChatSystemMessage, + LanguageModelUserMessage: extHostTypes.LanguageModelChatUserMessage, + LanguageModelAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage, + LanguageModelError: extHostTypes.LanguageModelError, NewSymbolName: extHostTypes.NewSymbolName, NewSymbolNameTag: extHostTypes.NewSymbolNameTag, InlineEdit: extHostTypes.InlineEdit, diff --git a/code/src/vs/workbench/api/common/extHost.common.services.ts b/code/src/vs/workbench/api/common/extHost.common.services.ts index faf45b596a7..d48b0d47391 100644 --- a/code/src/vs/workbench/api/common/extHost.common.services.ts +++ b/code/src/vs/workbench/api/common/extHost.common.services.ts @@ -28,11 +28,15 @@ import { ILoggerService } from 'vs/platform/log/common/log'; import { ExtHostVariableResolverProviderService, IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; import { ExtHostLocalizationService, IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService'; import { ExtHostManagedSockets, IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets'; +import { ExtHostAuthentication, IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; +import { ExtHostLanguageModels, IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed); registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed); registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService, InstantiationType.Delayed); registerSingleton(IExtHostCommands, ExtHostCommands, InstantiationType.Eager); +registerSingleton(IExtHostAuthentication, ExtHostAuthentication, InstantiationType.Eager); +registerSingleton(IExtHostLanguageModels, ExtHostLanguageModels, InstantiationType.Eager); registerSingleton(IExtHostConfiguration, ExtHostConfiguration, InstantiationType.Eager); registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem, InstantiationType.Eager); registerSingleton(IExtHostDebugService, WorkerExtHostDebugService, InstantiationType.Eager); diff --git a/code/src/vs/workbench/api/common/extHost.protocol.ts b/code/src/vs/workbench/api/common/extHost.protocol.ts index 29b99b8cc41..75fd8f80b89 100644 --- a/code/src/vs/workbench/api/common/extHost.protocol.ts +++ b/code/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,12 +50,12 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { IChatAgentCommand, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { IChatDynamicRequest, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { DebugConfigurationProviderTriggerKind, MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; +import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatFollowup, IInlineChatProgressItem, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -135,6 +135,7 @@ export type CommentThreadChanges = Partial<{ collapseState: languages.CommentThreadCollapsibleState; canReply: boolean; state: languages.CommentThreadState; + applicability: languages.CommentThreadApplicability; isTemplate: boolean; }>; @@ -145,7 +146,7 @@ export interface MainThreadCommentsShape extends IDisposable { $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, isTemplate: boolean): languages.CommentThread | undefined; $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; - $updateCommentingRanges(handle: number): void; + $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint): void; } export interface AuthenticationForceNewSessionOptions { @@ -414,7 +415,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; - $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], id: string, metadata: IPasteEditProviderMetadataDto): void; + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IPasteEditProviderMetadataDto): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string, supportRanges: boolean): void; $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; @@ -437,7 +438,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerSelectionRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerTypeHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; - $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], id: string | undefined, metadata?: IDocumentDropEditProviderMetadata): void; + $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[], metadata?: IDocumentDropEditProviderMetadata): void; $resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; @@ -698,7 +699,8 @@ export const enum TabInputKind { WebviewEditorInput, TerminalEditorInput, InteractiveEditorInput, - ChatEditorInput + ChatEditorInput, + MultiDiffEditorInput } export const enum TabModelOperationKind { @@ -766,11 +768,16 @@ export interface ChatEditorInputDto { providerId: string; } +export interface MultiDiffEditorInputDto { + kind: TabInputKind.MultiDiffEditorInput; + diffEditors: TextDiffInputDto[]; +} + export interface TabInputDto { kind: TabInputKind.TerminalEditorInput; } -export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | TextMergeInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto | WebviewInputDto | InteractiveEditorInputDto | ChatEditorInputDto | TabInputDto; +export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | MultiDiffEditorInputDto | TextMergeInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto | WebviewInputDto | InteractiveEditorInputDto | ChatEditorInputDto | TabInputDto; export interface MainThreadEditorTabsShape extends IDisposable { // manage tabs: move, close, rearrange etc @@ -1126,6 +1133,8 @@ export interface VariablesResult { name: string; value: string; type?: string; + language?: string; + expression?: string; hasNamedChildren: boolean; indexedChildrenCount: number; extensionId: string; @@ -1189,19 +1198,18 @@ export interface MainThreadLanguageModelsShape extends IDisposable { } export interface ExtHostLanguageModelsShape { - $updateLanguageModels(data: { added?: string[]; removed?: string[] }): void; + $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[]; removed?: string[] }): void; $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void; $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise; } export interface IExtensionChatAgentMetadata extends Dto { - hasSlashCommands?: boolean; hasFollowups?: boolean; } export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void; $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1228,8 +1236,7 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise; + $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; @@ -1277,11 +1284,6 @@ export interface MainThreadUrlsShape extends IDisposable { export interface IChatDto { id: number; - requesterUsername: string; - requesterAvatarIconUri?: UriComponents; - responderUsername: string; - responderAvatarIconUri?: UriComponents; - inputPlaceholder?: string; } export interface IChatRequestDto { @@ -1315,7 +1317,6 @@ export type IChatProgressDto = export interface MainThreadChatShape extends IDisposable { $registerChatProvider(handle: number, id: string): Promise; $acceptChatState(sessionId: number, state: any): Promise; - $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): void; $unregisterChatProvider(handle: number): Promise; $transferChatSession(sessionId: number, toWorkspace: UriComponents): void; } @@ -1403,6 +1404,7 @@ export interface MainThreadLabelServiceShape extends IDisposable { export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; + $registerAITextSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; $unregisterProvider(handle: number): void; $handleFileMatch(handle: number, session: number, data: UriComponents[]): void; @@ -1816,6 +1818,7 @@ export interface ExtHostSecretStateShape { export interface ExtHostSearchShape { $enableExtensionHostSearch(): void; $provideFileSearchResults(handle: number, session: number, query: search.IRawQuery, token: CancellationToken): Promise; + $provideAITextSearchResults(handle: number, session: number, query: search.IRawAITextQuery, token: CancellationToken): Promise; $provideTextSearchResults(handle: number, session: number, query: search.IRawTextQuery, token: CancellationToken): Promise; $clearCache(cacheKey: string): Promise; } @@ -2065,16 +2068,26 @@ export type ITypeHierarchyItemDto = Dto; export interface IPasteEditProviderMetadataDto { readonly supportsCopy: boolean; readonly supportsPaste: boolean; + readonly supportsResolve: boolean; + + readonly providedPasteEditKinds?: readonly string[]; readonly copyMimeTypes?: readonly string[]; readonly pasteMimeTypes?: readonly string[]; } +export interface IDocumentPasteContextDto { + readonly only: string | undefined; + readonly triggerKind: languages.DocumentPasteTriggerKind; + +} + export interface IPasteEditDto { - label: string; - detail: string; + _cacheId?: ChainedCacheId; + title: string; + kind: { value: string } | undefined; insertText: string | { snippet: string }; additionalEdit?: IWorkspaceEditDto; - yieldTo?: readonly languages.DropYieldTo[]; + yieldTo?: readonly string[]; } export interface IDocumentDropEditProviderMetadata { @@ -2082,10 +2095,11 @@ export interface IDocumentDropEditProviderMetadata { } export interface IDocumentOnDropEditDto { - label: string; + title: string; + kind: string | undefined; insertText: string | { snippet: string }; additionalEdit?: IWorkspaceEditDto; - yieldTo?: readonly languages.DropYieldTo[]; + yieldTo?: readonly string[]; } export interface ExtHostLanguageFeaturesShape { @@ -2108,7 +2122,9 @@ export interface ExtHostLanguageFeaturesShape { $resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<{ edit?: IWorkspaceEditDto; command?: ICommandDto }>; $releaseCodeActions(handle: number, cacheId: number): void; $prepareDocumentPaste(handle: number, uri: UriComponents, ranges: readonly IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise; - $providePasteEdits(handle: number, requestId: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise; + $providePasteEdits(handle: number, requestId: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, context: IDocumentPasteContextDto, token: CancellationToken): Promise; + $resolvePasteEdit(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: IWorkspaceEditDto }>; + $releasePasteEdits(handle: number, cacheId: number): void; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: languages.FormattingOptions, token: CancellationToken): Promise; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise; $provideDocumentRangesFormattingEdits(handle: number, resource: UriComponents, range: IRange[], options: languages.FormattingOptions, token: CancellationToken): Promise; @@ -2127,7 +2143,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void; + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: languages.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; @@ -2150,7 +2166,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySupertypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $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; + $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; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; @@ -2346,14 +2362,14 @@ export type IDebugSessionDto = IDebugSessionFullDto | DebugSessionUUID; export interface IThreadFocusDto { kind: 'thread'; sessionId: string; - threadId: number | undefined; + threadId: number; } export interface IStackFrameFocusDto { kind: 'stackFrame'; sessionId: string; - threadId: number | undefined; - frameId: number | undefined; + threadId: number; + frameId: number; } @@ -2671,13 +2687,10 @@ export interface ExtHostTestingShape { $publishTestResults(results: ISerializedTestResults[]): void; /** Expands a test item's children, by the given number of levels. */ $expandTest(testId: string, levels: number): Promise; - /** Requests file coverage for a test run. Errors if not available. */ - $provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise; - /** - * Requests coverage details for the file index in coverage data for the run. - * Requires file coverage to have been previously requested via $provideFileCoverage. - */ - $resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise; + /** Requests coverage details for a test run. Errors if not available. */ + $getCoverageDetails(coverageId: string, token: CancellationToken): Promise; + /** Disposes resources associated with a test run. */ + $disposeRun(runId: string): void; /** Configures a test run config. */ $configureRunProfile(controllerId: string, configId: number): void; /** Asks the controller to refresh its tests */ @@ -2748,7 +2761,7 @@ export interface MainThreadTestingShape { /** Appends raw output to the test run.. */ $appendOutputToRun(runId: string, taskId: string, output: VSBuffer, location?: ILocationDto, testId?: string): void; /** Triggered when coverage is added to test results. */ - $signalCoverageAvailable(runId: string, taskId: string, available: boolean): void; + $appendCoverage(runId: string, taskId: string, coverage: IFileCoverage.Serialized): void; /** Signals a task in a test run started. */ $startedTestRunTask(runId: string, task: ITestRunTask): void; /** Signals a task in a test run ended. */ diff --git a/code/src/vs/workbench/api/common/extHostAuthentication.ts b/code/src/vs/workbench/api/common/extHostAuthentication.ts index 84ddbd6fa55..1c562edf76a 100644 --- a/code/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/code/src/vs/workbench/api/common/extHostAuthentication.ts @@ -5,10 +5,15 @@ import type * as vscode from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; -import { IMainContext, MainContext, MainThreadAuthenticationShape, ExtHostAuthenticationShape } from 'vs/workbench/api/common/extHost.protocol'; +import { MainContext, MainThreadAuthenticationShape, ExtHostAuthenticationShape } from 'vs/workbench/api/common/extHost.protocol'; import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; + +export interface IExtHostAuthentication extends ExtHostAuthentication { } +export const IExtHostAuthentication = createDecorator('IExtHostAuthentication'); interface ProviderWithMetadata { label: string; @@ -17,6 +22,9 @@ interface ProviderWithMetadata { } export class ExtHostAuthentication implements ExtHostAuthenticationShape { + + declare _serviceBrand: undefined; + private _proxy: MainThreadAuthenticationShape; private _authenticationProviders: Map = new Map(); @@ -26,8 +34,10 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _getSessionTaskSingler = new TaskSingler(); private _getSessionsTaskSingler = new TaskSingler>(); - constructor(mainContext: IMainContext) { - this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService + ) { + this._proxy = extHostRpc.getProxy(MainContext.MainThreadAuthentication); } async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions & ({ createIfNone: true } | { forceNewSession: true } | { forceNewSession: vscode.AuthenticationForceNewSessionOptions })): Promise; diff --git a/code/src/vs/workbench/api/common/extHostChat.ts b/code/src/vs/workbench/api/common/extHostChat.ts index d125a8b8f79..036d64b14d2 100644 --- a/code/src/vs/workbench/api/common/extHostChat.ts +++ b/code/src/vs/workbench/api/common/extHostChat.ts @@ -58,10 +58,6 @@ export class ExtHostChat implements ExtHostChatShape { this._proxy.$transferChatSession(sessionId, newWorkspace); } - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest): void { - this._proxy.$sendRequestToProvider(providerId, message); - } - async $prepareChat(handle: number, token: CancellationToken): Promise { const entry = this._chatProvider.get(handle); if (!entry) { @@ -78,11 +74,6 @@ export class ExtHostChat implements ExtHostChatShape { return { id, - requesterUsername: session.requester?.name, - requesterAvatarIconUri: session.requester?.icon, - responderUsername: session.responder?.name, - responderAvatarIconUri: session.responder?.icon, - inputPlaceholder: session.inputPlaceholder, }; } diff --git a/code/src/vs/workbench/api/common/extHostChatAgents2.ts b/code/src/vs/workbench/api/common/extHostChatAgents2.ts index 8166c1e7d33..7a4c69dc8f3 100644 --- a/code/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/code/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -21,7 +21,7 @@ import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEnt import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IChatAgentCommand, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatFollowup, IChatProgress, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -171,7 +171,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, name, {}); + this._proxy.$registerAgent(handle, extension.identifier, name, {}, isProposedApiEnabled(extension, 'chatParticipantAdditions')); return agent.apiAgent; } @@ -213,7 +213,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { } catch (e) { this._logService.error(e, agent.extension); - return { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; + return { errorDetails: { message: localize('errorResponse', "Error from participant: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; } finally { stream.close(); @@ -245,38 +245,23 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { + async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { - // this is OK, the agent might have disposed while the request was in flight - return []; + return Promise.resolve([]); } const convertedHistory = await this.prepareHistoryTurns(agent.id, context); - try { - return await agent.provideSlashCommands({ history: convertedHistory }, token); - } catch (err) { - const msg = toErrorMessage(err); - this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] Error while providing slash commands: ${msg}`); - return []; - } - } - - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - return Promise.resolve([]); - } const ehResult = typeConvert.ChatAgentResult.to(result); - return (await agent.provideFollowups(ehResult, token)) + return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token)) .filter(f => { // The followup must refer to a participant that exists from the same extension const isValid = !f.participant || Iterable.some( this._agents.values(), a => a.id === f.participant && ExtensionIdentifier.equals(a.extension.identifier, agent.extension.identifier)); if (!isValid) { - this._logService.warn(`[@${agent.id}] ChatFollowup refers to an invalid participant: ${f.participant}`); + this._logService.warn(`[@${agent.id}] ChatFollowup refers to an unknown participant: ${f.participant}`); } return isValid; }) @@ -352,7 +337,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { class ExtHostChatAgent { - private _commandProvider: vscode.ChatCommandProvider | undefined; private _followupProvider: vscode.ChatFollowupProvider | undefined; private _description: string | undefined; private _fullName: string | undefined; @@ -369,6 +353,7 @@ class ExtHostChatAgent { private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _isSticky: boolean | undefined; + private _requester: vscode.ChatRequesterInformation | undefined; constructor( public readonly extension: IExtensionDescription, @@ -394,35 +379,12 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - async provideSlashCommands(context: vscode.ChatContext, token: CancellationToken): Promise { - if (!this._commandProvider) { - return []; - } - const result = await this._commandProvider.provideCommands(context, token); - if (!result) { - return []; - } - return result - .map(c => { - if ('isSticky2' in c) { - checkProposedApiEnabled(this.extension, 'chatParticipantAdditions'); - } - - return { - name: c.name, - description: c.description ?? '', - followupPlaceholder: c.isSticky2?.placeholder, - isSticky: c.isSticky2?.isSticky ?? c.isSticky, - sampleRequest: c.sampleRequest - } satisfies IChatAgentCommand; - }); - } - - async provideFollowups(result: vscode.ChatResult, token: CancellationToken): Promise { + async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: CancellationToken): Promise { if (!this._followupProvider) { return []; } - const followups = await this._followupProvider.provideFollowups(result, token); + + const followups = await this._followupProvider.provideFollowups(result, context, token); if (!followups) { return []; } @@ -475,7 +437,7 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - description: this._description ?? '', + description: this._description, fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : @@ -485,9 +447,7 @@ class ExtHostChatAgent { 'dark' in this._iconPath ? this._iconPath.dark : undefined, themeIcon: this._iconPath instanceof extHostTypes.ThemeIcon ? this._iconPath : undefined, - hasSlashCommands: this._commandProvider !== undefined, hasFollowups: this._followupProvider !== undefined, - isDefault: this._isDefault, isSecondary: this._isSecondary, helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix), helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), @@ -495,6 +455,7 @@ class ExtHostChatAgent { sampleRequest: this._sampleRequest, supportIssueReporting: this._supportIssueReporting, isSticky: this._isSticky, + requester: this._requester }); updateScheduled = false; }); @@ -535,13 +496,6 @@ class ExtHostChatAgent { assertType(typeof v === 'function', 'Invalid request handler'); that._requestHandler = v; }, - get commandProvider() { - return that._commandProvider; - }, - set commandProvider(v) { - that._commandProvider = v; - updateMetadataSoon(); - }, get followupProvider() { return that._followupProvider; }, @@ -564,10 +518,6 @@ class ExtHostChatAgent { }, set helpTextPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPrefix is only available on the default chat agent'); - } - that._helpTextPrefix = v; updateMetadataSoon(); }, @@ -577,10 +527,6 @@ class ExtHostChatAgent { }, set helpTextVariablesPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextVariablesPrefix is only available on the default chat agent'); - } - that._helpTextVariablesPrefix = v; updateMetadataSoon(); }, @@ -590,10 +536,6 @@ class ExtHostChatAgent { }, set helpTextPostfix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPostfix is only available on the default chat agent'); - } - that._helpTextPostfix = v; updateMetadataSoon(); }, @@ -662,9 +604,15 @@ class ExtHostChatAgent { that._isSticky = v; updateMetadataSoon(); }, + set requester(v) { + that._requester = v; + updateMetadataSoon(); + }, + get requester() { + return that._requester; + }, dispose() { disposed = true; - that._commandProvider = undefined; that._followupProvider = undefined; that._onDidReceiveFeedback.dispose(); that._proxy.$unregisterAgent(that._handle); diff --git a/code/src/vs/workbench/api/common/extHostComments.ts b/code/src/vs/workbench/api/common/extHostComments.ts index 0f04b3f2455..72cf9d3de72 100644 --- a/code/src/vs/workbench/api/common/extHostComments.ts +++ b/code/src/vs/workbench/api/common/extHostComments.ts @@ -20,6 +20,7 @@ import type * as vscode from 'vscode'; import { ExtHostCommentsShape, IMainContext, MainContext, CommentThreadChanges, CommentChanges } from './extHost.protocol'; import { ExtHostCommands } from './extHostCommands'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; type ProviderHandle = number; @@ -53,16 +54,17 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return commentController.value; } else if (arg && arg.$mid === MarshalledId.CommentThread) { - const commentController = this._commentControllers.get(arg.commentControlHandle); + const marshalledCommentThread: MarshalledCommentThread = arg; + const commentController = this._commentControllers.get(marshalledCommentThread.commentControlHandle); if (!commentController) { - return arg; + return marshalledCommentThread; } - const commentThread = commentController.getCommentThread(arg.commentThreadHandle); + const commentThread = commentController.getCommentThread(marshalledCommentThread.commentThreadHandle); if (!commentThread) { - return arg; + return marshalledCommentThread; } return commentThread.value; @@ -194,16 +196,16 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo commentController?.$deleteCommentThread(commentThreadHandle); } - $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<{ ranges: IRange[]; fileComments: boolean } | undefined> { + async $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<{ ranges: IRange[]; fileComments: boolean } | undefined> { const commentController = this._commentControllers.get(commentControllerHandle); if (!commentController || !commentController.commentingRangeProvider) { return Promise.resolve(undefined); } - const document = documents.getDocument(URI.revive(uriComponents)); + const document = await documents.ensureDocumentData(URI.revive(uriComponents)); return asPromise(async () => { - const rangesResult = await (commentController.commentingRangeProvider as vscode.CommentingRangeProvider2).provideCommentingRanges(document, token); + const rangesResult = await (commentController.commentingRangeProvider as vscode.CommentingRangeProvider2).provideCommentingRanges(document.document, token); let ranges: { ranges: vscode.Range[]; fileComments: boolean } | undefined; if (Array.isArray(rangesResult)) { ranges = { @@ -263,6 +265,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo canReply: boolean; state: vscode.CommentThreadState; isTemplate: boolean; + applicability: vscode.CommentThreadApplicability; }>; class ExtHostCommentThread implements vscode.CommentThread2 { @@ -366,15 +369,21 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._onDidUpdateCommentThread.fire(); } - private _state?: vscode.CommentThreadState; + private _state?: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }; - get state(): vscode.CommentThreadState { + get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return this._state!; } - set state(newState: vscode.CommentThreadState) { + set state(newState: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { this._state = newState; - this.modifications.state = newState; + if (typeof newState === 'object') { + checkProposedApiEnabled(this.extensionDescription, 'commentThreadApplicability'); + this.modifications.state = newState.resolved; + this.modifications.applicability = newState.applicability; + } else { + this.modifications.state = newState; + } this._onDidUpdateCommentThread.fire(); } @@ -452,8 +461,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set contextValue(value: string | undefined) { that.contextValue = value; }, get label() { return that.label; }, set label(value: string | undefined) { that.label = value; }, - get state() { return that.state; }, - set state(value: vscode.CommentThreadState) { that.state = value; }, + get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return that.state; }, + set state(value: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { that.state = value; }, dispose: () => { that.dispose(); } @@ -508,6 +517,9 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo if (modified('state')) { formattedModifications.state = convertToState(this._state); } + if (modified('applicability')) { + formattedModifications.applicability = convertToRelevance(this._state); + } if (modified('isTemplate')) { formattedModifications.isTemplate = this._isTemplate; } @@ -565,7 +577,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set commentingRangeProvider(provider: vscode.CommentingRangeProvider | undefined) { this._commentingRangeProvider = provider; - proxy.$updateCommentingRanges(this.handle); + if (provider?.resourceHints) { + checkProposedApiEnabled(this._extension, 'commentingRangeHint'); + } + proxy.$updateCommentingRanges(this.handle, provider?.resourceHints); } private _reactionHandler?: ReactionHandler; @@ -761,9 +776,16 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return languages.CommentThreadCollapsibleState.Collapsed; } - function convertToState(kind: vscode.CommentThreadState | undefined): languages.CommentThreadState { - if (kind !== undefined) { - switch (kind) { + function convertToState(kind: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined): languages.CommentThreadState { + let resolvedKind: vscode.CommentThreadState | undefined; + if (typeof kind === 'object') { + resolvedKind = kind.resolved; + } else { + resolvedKind = kind; + } + + if (resolvedKind !== undefined) { + switch (resolvedKind) { case types.CommentThreadState.Unresolved: return languages.CommentThreadState.Unresolved; case types.CommentThreadState.Resolved: @@ -773,5 +795,22 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return languages.CommentThreadState.Unresolved; } + function convertToRelevance(kind: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined): languages.CommentThreadApplicability { + let applicabilityKind: vscode.CommentThreadApplicability | undefined = undefined; + if (typeof kind === 'object') { + applicabilityKind = kind.applicability; + } + + if (applicabilityKind !== undefined) { + switch (applicabilityKind) { + case types.CommentThreadApplicability.Current: + return languages.CommentThreadApplicability.Current; + case types.CommentThreadApplicability.Outdated: + return languages.CommentThreadApplicability.Outdated; + } + } + return languages.CommentThreadApplicability.Current; + } + return new ExtHostCommentsImpl(); } diff --git a/code/src/vs/workbench/api/common/extHostDebugService.ts b/code/src/vs/workbench/api/common/extHostDebugService.ts index 38d5f2a3205..e38d3ee11b1 100644 --- a/code/src/vs/workbench/api/common/extHostDebugService.ts +++ b/code/src/vs/workbench/api/common/extHostDebugService.ts @@ -15,7 +15,7 @@ import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThre import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, ThreadFocus, StackFrameFocus, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; +import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, Thread, StackFrame, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebugVisualization, IDebugVisualizationContext, IDebuggerContribution, DebugVisualizationType, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; @@ -44,8 +44,8 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { onDidReceiveDebugSessionCustomEvent: Event; onDidChangeBreakpoints: Event; breakpoints: vscode.Breakpoint[]; - onDidChangeStackFrameFocus: Event; - stackFrameFocus: vscode.ThreadFocus | vscode.StackFrameFocus | undefined; + onDidChangeActiveStackItem: Event; + activeStackItem: vscode.Thread | vscode.StackFrame | undefined; addBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; removeBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; @@ -97,8 +97,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private readonly _onDidChangeBreakpoints: Emitter; - private _stackFrameFocus: vscode.ThreadFocus | vscode.StackFrameFocus | undefined; - private readonly _onDidChangeStackFrameFocus: Emitter; + private _activeStackItem: vscode.Thread | vscode.StackFrame | undefined; + private readonly _onDidChangeActiveStackItem: Emitter; private _debugAdapters: Map; private _debugAdaptersTrackers: Map; @@ -144,7 +144,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E this._onDidChangeBreakpoints = new Emitter(); - this._onDidChangeStackFrameFocus = new Emitter(); + this._onDidChangeActiveStackItem = new Emitter(); this._activeDebugConsole = new ExtHostDebugConsole(this._debugServiceProxy); @@ -278,12 +278,12 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E // extension debug API - get stackFrameFocus(): vscode.ThreadFocus | vscode.StackFrameFocus | undefined { - return this._stackFrameFocus; + get activeStackItem(): vscode.Thread | vscode.StackFrame | undefined { + return this._activeStackItem; } - get onDidChangeStackFrameFocus(): Event { - return this._onDidChangeStackFrameFocus.event; + get onDidChangeActiveStackItem(): Event { + return this._onDidChangeActiveStackItem.event; } get onDidChangeBreakpoints(): Event { @@ -768,21 +768,19 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E this.fireBreakpointChanges(a, r, c); } - public async $acceptStackFrameFocus(focusDto: IThreadFocusDto | IStackFrameFocusDto): Promise { - let focus: ThreadFocus | StackFrameFocus; - const session = focusDto.sessionId ? await this.getSession(focusDto.sessionId) : undefined; - if (!session) { - throw new Error('no DebugSession found for debug focus context'); - } - - if (focusDto.kind === 'thread') { - focus = new ThreadFocus(session.api, focusDto.threadId); - } else { - focus = new StackFrameFocus(session.api, focusDto.threadId, focusDto.frameId); + public async $acceptStackFrameFocus(focusDto: IThreadFocusDto | IStackFrameFocusDto | undefined): Promise { + let focus: vscode.Thread | vscode.StackFrame | undefined; + if (focusDto) { + const session = await this.getSession(focusDto.sessionId); + if (focusDto.kind === 'thread') { + focus = new Thread(session.api, focusDto.threadId); + } else { + focus = new StackFrame(session.api, focusDto.threadId, focusDto.frameId); + } } - this._stackFrameFocus = focus; - this._onDidChangeStackFrameFocus.fire(this._stackFrameFocus); + this._activeStackItem = focus; + this._onDidChangeActiveStackItem.fire(this._activeStackItem); } public $provideDebugConfigurations(configProviderHandle: number, folderUri: UriComponents | undefined, token: CancellationToken): Promise { diff --git a/code/src/vs/workbench/api/common/extHostEditorTabs.ts b/code/src/vs/workbench/api/common/extHostEditorTabs.ts index 1bdb2bf11e4..d0e035825a6 100644 --- a/code/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/code/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TabOperation } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { ChatEditorTabInput, CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextMergeTabInput, TextTabInput, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { ChatEditorTabInput, CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextMergeTabInput, TextTabInput, WebviewEditorTabInput, TextMultiDiffTabInput } from 'vs/workbench/api/common/extHostTypes'; import type * as vscode from 'vscode'; export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { @@ -21,7 +21,7 @@ export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { export const IExtHostEditorTabs = createDecorator('IExtHostEditorTabs'); -type AnyTabInput = TextTabInput | TextDiffTabInput | CustomEditorTabInput | NotebookEditorTabInput | NotebookDiffEditorTabInput | WebviewEditorTabInput | TerminalEditorTabInput | InteractiveWindowInput | ChatEditorTabInput; +type AnyTabInput = TextTabInput | TextDiffTabInput | TextMultiDiffTabInput | CustomEditorTabInput | NotebookEditorTabInput | NotebookDiffEditorTabInput | WebviewEditorTabInput | TerminalEditorTabInput | InteractiveWindowInput | ChatEditorTabInput; class ExtHostEditorTab { private _apiObject: vscode.Tab | undefined; @@ -100,6 +100,8 @@ class ExtHostEditorTab { return new InteractiveWindowInput(URI.revive(this._dto.input.uri), URI.revive(this._dto.input.inputBoxUri)); case TabInputKind.ChatEditorInput: return new ChatEditorTabInput(this._dto.input.providerId); + case TabInputKind.MultiDiffEditorInput: + return new TextMultiDiffTabInput(this._dto.input.diffEditors.map(diff => new TextDiffTabInput(URI.revive(diff.original), URI.revive(diff.modified)))); default: return undefined; } diff --git a/code/src/vs/workbench/api/common/extHostExtensionService.ts b/code/src/vs/workbench/api/common/extHostExtensionService.ts index 91f910aa4c8..2679364d945 100644 --- a/code/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/code/src/vs/workbench/api/common/extHostExtensionService.ts @@ -36,6 +36,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; +import { IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { Emitter, Event } from 'vs/base/common/event'; import { IExtensionActivationHost, checkActivateWorkspaceContainsExtension } from 'vs/workbench/services/extensions/common/workspaceContains'; import { ExtHostSecretState, IExtHostSecretState } from 'vs/workbench/api/common/extHostSecretState'; @@ -116,6 +117,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private readonly _storagePath: IExtensionStoragePaths; private readonly _activator: ExtensionsActivator; private _extensionPathIndex: Promise | null; + private _realPathCache = new Map>(); private readonly _resolvers: { [authorityPrefix: string]: vscode.RemoteAuthorityResolver }; @@ -136,6 +138,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, @IExtHostLocalizationService extHostLocalizationService: IExtHostLocalizationService, @IExtHostManagedSockets private readonly _extHostManagedSockets: IExtHostManagedSockets, + @IExtHostLanguageModels private readonly _extHostLanguageModels: IExtHostLanguageModels, ) { super(); this._hostUtils = hostUtils; @@ -330,11 +333,16 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } /** - * Applies realpath to file-uris and returns all others uris unmodified + * Applies realpath to file-uris and returns all others uris unmodified. + * The real path is cached for the lifetime of the extension host. */ private async _realPathExtensionUri(uri: URI): Promise { if (uri.scheme === Schemas.file && this._hostUtils.fsRealpath) { - const realpathValue = await this._hostUtils.fsRealpath(uri.fsPath); + const fsPath = uri.fsPath; + if (!this._realPathCache.has(fsPath)) { + this._realPathCache.set(fsPath, this._hostUtils.fsRealpath(fsPath)); + } + const realpathValue = await this._realPathCache.get(fsPath)!; return URI.file(realpathValue); } return uri; @@ -489,6 +497,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { + const lanuageModelAccessInformation = this._extHostLanguageModels.createLanguageModelAccessInformation(extensionDescription); const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage); const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); const secrets = new ExtensionSecrets(extensionDescription, this._secretState); @@ -517,6 +526,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme workspaceState, secrets, subscriptions: [], + get languageModelAccessInformation() { return lanuageModelAccessInformation; }, get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, @@ -982,10 +992,13 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return result; } - public $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise { + public async $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise { extensionsDelta.toAdd.forEach((extension) => (extension).extensionLocation = URI.revive(extension.extensionLocation)); const { globalRegistry, myExtensions } = applyExtensionsDelta(this._activationEventsReader, this._globalRegistry, this._myRegistry, extensionsDelta); + const newSearchTree = await this._createExtensionPathIndex(myExtensions); + const extensionsPaths = await this.getExtensionPathIndex(); + extensionsPaths.setSearchTree(newSearchTree); this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions()); this._myRegistry.set(myExtensions); diff --git a/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts index d6bc4f793aa..fe84fef3af9 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, isFalsyOrEmpty, isNonEmptyArray } from 'vs/base/common/arrays'; +import { asArray, coalesce, isFalsyOrEmpty, isNonEmptyArray } from 'vs/base/common/arrays'; import { raceCancellationError } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -32,7 +32,7 @@ import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { CodeActionKind, CompletionList, Disposable, DocumentSymbol, InlineCompletionTriggerKind, InternalDataTransferItem, Location, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType, InlineEditTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { CodeActionKind, CompletionList, Disposable, DocumentPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; import { Cache } from './cache'; @@ -537,9 +537,7 @@ class CodeActionAdapter { class DocumentPasteEditProvider { - public static toInternalProviderId(extId: string, editId: string): string { - return extId + '.' + editId; - } + private readonly _cache = new Cache('DocumentPasteEdit'); constructor( private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, @@ -570,9 +568,9 @@ class DocumentPasteEditProvider { return typeConvert.DataTransfer.from(entries); } - async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, context: extHostProtocol.IDocumentPasteContextDto, token: CancellationToken): Promise { if (!this._provider.provideDocumentPasteEdits) { - return; + return []; } const doc = this._documents.getDocument(resource); @@ -582,20 +580,40 @@ class DocumentPasteEditProvider { return (await this._proxy.$resolvePasteFileData(this._handle, requestId, id)).buffer; }); - const edit = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, token); - if (!edit) { - return; + const edits = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, { + only: context.only ? new DocumentPasteEditKind(context.only) : undefined, + triggerKind: context.triggerKind, + }, token); + if (!edits || token.isCancellationRequested) { + return []; } - return { - label: edit.label ?? localize('defaultPasteLabel', "Paste using '{0}' extension", this._extension.displayName || this._extension.name), - detail: this._extension.displayName || this._extension.name, - yieldTo: edit.yieldTo?.map(yTo => { - return 'mimeType' in yTo ? yTo : { providerId: DocumentPasteEditProvider.toInternalProviderId(yTo.extensionId, yTo.providerId) }; - }), + const cacheId = this._cache.add(edits); + + return edits.map((edit, i): extHostProtocol.IPasteEditDto => ({ + _cacheId: [cacheId, i], + title: edit.title ?? localize('defaultPasteLabel', "Paste using '{0}' extension", this._extension.displayName || this._extension.name), + kind: edit.kind, + yieldTo: edit.yieldTo?.map(x => x.value), insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, - }; + })); + } + + async resolvePasteEdit(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: extHostProtocol.IWorkspaceEditDto }> { + const [sessionId, itemId] = id; + const item = this._cache.get(sessionId, itemId); + if (!item || !this._provider.resolveDocumentPasteEdit) { + return {}; // this should not happen... + } + + const resolvedItem = (await this._provider.resolveDocumentPasteEdit(item, token)) ?? item; + const additionalEdit = resolvedItem.additionalEdit ? typeConvert.WorkspaceEdit.from(resolvedItem.additionalEdit, undefined) : undefined; + return { additionalEdit }; + } + + releasePasteEdits(id: number): any { + this._cache.delete(id); } } @@ -1230,7 +1248,7 @@ class InlineCompletionAdapterBase { handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { } - handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { } + handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1345,11 +1363,12 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } } - override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { + override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { if (this._provider.handleDidPartiallyAcceptCompletionItem && this._isAdditionsProposedApiEnabled) { this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, acceptedCharacters); + this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, typeConvert.PartialAcceptInfo.to(info)); } } } @@ -1939,10 +1958,6 @@ class TypeHierarchyAdapter { class DocumentOnDropEditAdapter { - public static toInternalProviderId(extId: string, editId: string): string { - return extId + '.' + editId; - } - constructor( private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, private readonly _documents: ExtHostDocuments, @@ -1951,25 +1966,25 @@ class DocumentOnDropEditAdapter { private readonly _extension: IExtensionDescription, ) { } - async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { const doc = this._documents.getDocument(uri); const pos = typeConvert.Position.to(position); const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (id) => { return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, id)).buffer; }); - const edit = await this._provider.provideDocumentDropEdits(doc, pos, dataTransfer, token); - if (!edit) { + const edits = await this._provider.provideDocumentDropEdits(doc, pos, dataTransfer, token); + if (!edits) { return undefined; } - return { - label: edit.label ?? localize('defaultDropLabel', "Drop using '{0}' extension", this._extension.displayName || this._extension.name), - yieldTo: edit.yieldTo?.map(yTo => { - return 'mimeType' in yTo ? yTo : { providerId: DocumentOnDropEditAdapter.toInternalProviderId(yTo.extensionId, yTo.providerId) }; - }), + + return asArray(edits).map((edit): extHostProtocol.IDocumentOnDropEditDto => ({ + title: edit.title ?? localize('defaultDropLabel', "Drop using '{0}' extension", this._extension.displayName || this._extension.name), + kind: edit.kind?.value, + yieldTo: edit.yieldTo?.map(x => x.value), insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, - }; + })); } } @@ -2489,9 +2504,9 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF }, undefined, undefined); } - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void { + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { - adapter.handlePartialAccept(pid, idx, acceptedCharacters); + adapter.handlePartialAccept(pid, idx, acceptedCharacters, info); }, undefined, undefined); } @@ -2691,13 +2706,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF const handle = this._nextHandle(); this._adapter.set(handle, new AdapterData(new DocumentOnDropEditAdapter(this._proxy, this._documents, provider, handle, extension), extension)); - const id = isProposedApiEnabled(extension, 'dropMetadata') && metadata ? DocumentOnDropEditAdapter.toInternalProviderId(extension.identifier.value, metadata.id) : undefined; - this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), id, isProposedApiEnabled(extension, 'dropMetadata') ? metadata : undefined); + this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), isProposedApiEnabled(extension, 'dropMetadata') ? metadata : undefined); return this._createDisposable(handle); } - $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { return this._withAdapter(handle, DocumentOnDropEditAdapter, adapter => Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); } @@ -2720,10 +2734,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { const handle = this._nextHandle(); this._adapter.set(handle, new AdapterData(new DocumentPasteEditProvider(this._proxy, this._documents, provider, handle, extension), extension)); - const internalId = DocumentPasteEditProvider.toInternalProviderId(extension.identifier.value, metadata.id); - this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector, extension), internalId, { + this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector, extension), { supportsCopy: !!provider.prepareDocumentPaste, supportsPaste: !!provider.provideDocumentPasteEdits, + supportsResolve: !!provider.resolveDocumentPasteEdit, + providedPasteEditKinds: metadata.providedPasteEditKinds?.map(x => x.value), copyMimeTypes: metadata.copyMimeTypes, pasteMimeTypes: metadata.pasteMimeTypes, }); @@ -2734,8 +2749,16 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.prepareDocumentPaste(URI.revive(resource), ranges, dataTransfer, token), undefined, token); } - $providePasteEdits(handle: number, requestId: number, resource: UriComponents, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(requestId, URI.revive(resource), ranges, dataTransferDto, token), undefined, token); + $providePasteEdits(handle: number, requestId: number, resource: UriComponents, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, context: extHostProtocol.IDocumentPasteContextDto, token: CancellationToken): Promise { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(requestId, URI.revive(resource), ranges, dataTransferDto, context, token), undefined, token); + } + + $resolvePasteEdit(handle: number, id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: extHostProtocol.IWorkspaceEditDto }> { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.resolvePasteEdit(id, token), {}, undefined); + } + + $releasePasteEdits(handle: number, cacheId: number): void { + this._withAdapter(handle, DocumentPasteEditProvider, adapter => Promise.resolve(adapter.releasePasteEdits(cacheId)), undefined, undefined); } // --- configuration diff --git a/code/src/vs/workbench/api/common/extHostLanguageModels.ts b/code/src/vs/workbench/api/common/extHostLanguageModels.ts index aa45f8f58aa..8221534013c 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -3,23 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostLanguageModelsShape, IMainContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { LanguageModelError } from 'vs/workbench/api/common/extHostTypes'; import type * as vscode from 'vscode'; import { Progress } from 'vs/platform/progress/common/progress'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { AsyncIterableSource } from 'vs/base/common/async'; +import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { localize } from 'vs/nls'; import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { CancellationError } from 'vs/base/common/errors'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; +import { ILogService } from 'vs/platform/log/common/log'; + +export interface IExtHostLanguageModels extends ExtHostLanguageModels { } + +export const IExtHostLanguageModels = createDecorator('IExtHostLanguageModels'); type LanguageModelData = { + readonly languageModelId: string; readonly extension: ExtensionIdentifier; readonly provider: vscode.ChatResponseProvider; }; @@ -36,43 +44,23 @@ class LanguageModelResponseStream { } } -class LanguageModelRequest { - - static fromError(err: Error): vscode.LanguageModelResponse { - return new LanguageModelRequest(Promise.reject(err), new CancellationTokenSource()).apiObject; - } +class LanguageModelResponse { - readonly apiObject: vscode.LanguageModelResponse; + readonly apiObject: vscode.LanguageModelChatResponse; private readonly _responseStreams = new Map(); private readonly _defaultStream = new AsyncIterableSource(); private _isDone: boolean = false; + private _isStreaming: boolean = false; + + constructor() { - constructor( - promise: Promise, - readonly cts: CancellationTokenSource - ) { const that = this; this.apiObject = { - result: promise, + // result: promise, stream: that._defaultStream.asyncIterable, - // responses: AsyncIterable[] // FUTURE responses per N + // streams: AsyncIterable[] // FUTURE responses per N }; - - promise.then(() => { - for (const stream of this._streams()) { - stream.resolve(); - } - }).catch(err => { - if (!(err instanceof Error)) { - err = new Error(toErrorMessage(err), { cause: err }); - } - for (const stream of this._streams()) { - stream.reject(err); - } - }).finally(() => { - this._isDone = true; - }); } private * _streams() { @@ -89,6 +77,7 @@ class LanguageModelRequest { if (this._isDone) { return; } + this._isStreaming = true; let res = this._responseStreams.get(fragment.index); if (!res) { if (this._responseStreams.size === 0) { @@ -102,10 +91,30 @@ class LanguageModelRequest { res.stream.emitOne(fragment.part); } + get isStreaming(): boolean { + return this._isStreaming; + } + + reject(err: Error): void { + this._isDone = true; + for (const stream of this._streams()) { + stream.reject(err); + } + } + + resolve(): void { + this._isDone = true; + for (const stream of this._streams()) { + stream.resolve(); + } + } + } export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { + declare _serviceBrand: undefined; + private static _idPool = 1; private readonly _proxy: MainThreadLanguageModelsShape; @@ -114,17 +123,16 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { readonly onDidChangeProviders = this._onDidChangeProviders.event; private readonly _languageModels = new Map(); - private readonly _languageModelIds = new Set(); // these are ALL models, not just the one in this EH + private readonly _allLanguageModelData = new Map(); // these are ALL models, not just the one in this EH private readonly _modelAccessList = new ExtensionIdentifierMap(); - private readonly _pendingRequest = new Map(); - + private readonly _pendingRequest = new Map(); constructor( - mainContext: IMainContext, - private readonly _logService: ILogService, - private readonly _extHostAuthentication: ExtHostAuthentication, + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @ILogService private readonly _logService: ILogService, + @IExtHostAuthentication private readonly _extHostAuthentication: IExtHostAuthentication, ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModels); + this._proxy = extHostRpc.getProxy(MainContext.MainThreadLanguageModels); } dispose(): void { @@ -135,7 +143,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { registerLanguageModel(extension: IExtensionDescription, identifier: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata): IDisposable { const handle = ExtHostLanguageModels._idPool++; - this._languageModels.set(handle, { extension: extension.identifier, provider }); + this._languageModels.set(handle, { extension: extension.identifier, provider, languageModelId: identifier }); let auth; if (metadata.auth) { auth = { @@ -145,6 +153,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } this._proxy.$registerLanguageModelProvider(handle, identifier, { extension: extension.identifier, + identifier: identifier, model: metadata.name ?? '', auth }); @@ -173,25 +182,25 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { //#region --- making request - $updateLanguageModels(data: { added?: string[] | undefined; removed?: string[] | undefined }): void { + $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[] | undefined; removed?: string[] | undefined }): void { const added: string[] = []; const removed: string[] = []; if (data.added) { - for (const id of data.added) { - this._languageModelIds.add(id); - added.push(id); + for (const metadata of data.added) { + this._allLanguageModelData.set(metadata.identifier, metadata); + added.push(metadata.model); } } if (data.removed) { for (const id of data.removed) { // clean up - this._languageModelIds.delete(id); + this._allLanguageModelData.delete(id); removed.push(id); // cancel pending requests for this model for (const [key, value] of this._pendingRequest) { if (value.languageModelId === id) { - value.res.cts.cancel(); + value.res.reject(new CancellationError()); this._pendingRequest.delete(key); } } @@ -202,10 +211,13 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { added: Object.freeze(added), removed: Object.freeze(removed) })); + + // TODO@jrieken@TylerLeonhardt - this is a temporary hack to populate the auth providers + data.added?.forEach(this._fakeAuthPopulate, this); } getLanguageModelIds(): string[] { - return Array.from(this._languageModelIds); + return Array.from(this._allLanguageModelData.keys()); } $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { @@ -227,86 +239,58 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - async makeChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelMessage[], options: Record, token: CancellationToken) { + async sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { const from = extension.identifier; - // const justification = options?.justification; // TODO@jrieken - const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, undefined); + const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, options.justification); - if (!metadata || !this._languageModelIds.has(languageModelId)) { - return LanguageModelRequest.fromError(new Error(`Language model ${languageModelId} is unknown`)); + if (!metadata || !this._allLanguageModelData.has(languageModelId)) { + throw LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); } if (this._isUsingAuth(from, metadata)) { - await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, undefined); + const success = await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, options.justification, options.silent); - if (!this._modelAccessList.get(from)?.has(metadata.extension)) { - return LanguageModelRequest.fromError(new Error('Access to chat has been revoked')); + if (!success || !this._modelAccessList.get(from)?.has(metadata.extension)) { + throw LanguageModelError.NoPermissions(`Language model '${languageModelId}' cannot be used by '${from.value}'.`); } } - const cts = new CancellationTokenSource(token); const requestId = (Math.random() * 1e6) | 0; - const requestPromise = this._proxy.$fetchResponse(from, languageModelId, requestId, messages.map(typeConvert.LanguageModelMessage.from), options ?? {}, cts.token); - const res = new LanguageModelRequest(requestPromise, cts); + const requestPromise = this._proxy.$fetchResponse(from, languageModelId, requestId, messages.map(typeConvert.LanguageModelMessage.from), options.modelOptions ?? {}, token); + + const barrier = new Barrier(); + + const res = new LanguageModelResponse(); this._pendingRequest.set(requestId, { languageModelId, res }); - requestPromise.finally(() => { + let error: Error | undefined; + + requestPromise.catch(err => { + if (barrier.isOpen()) { + // we received an error while streaming. this means we need to reject the "stream" + // because we have already returned the request object + res.reject(err); + } else { + error = err; + } + }).finally(() => { this._pendingRequest.delete(requestId); - cts.dispose(); + res.resolve(); + barrier.open(); }); - return res.apiObject; - } - - async requestLanguageModelAccess(extension: IExtensionDescription, languageModelId: string, options?: vscode.LanguageModelAccessOptions): Promise { - const from = extension.identifier; - const justification = options?.justification; - const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, justification); - - if (!metadata) { - throw new Error(`Language model '${languageModelId}' NOT found`); - } + await barrier.wait(); - if (this._isUsingAuth(from, metadata)) { - await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, justification); + if (error) { + throw new LanguageModelError( + `Language model '${languageModelId}' errored, check cause for more details`, + 'Unknown', + error + ); } - const that = this; - - return { - get model() { - return metadata.model; - }, - get isRevoked() { - return (that._isUsingAuth(from, metadata) && !that._modelAccessList.get(from)?.has(metadata.extension)) || !that._languageModelIds.has(languageModelId); - }, - get onDidChangeAccess() { - const onDidRemoveLM = Event.filter(that._onDidChangeProviders.event, e => e.removed.includes(languageModelId)); - const onDidChangeModelAccess = Event.filter(that._onDidChangeModelAccess.event, e => ExtensionIdentifier.equals(e.from, from) && ExtensionIdentifier.equals(e.to, metadata.extension)); - return Event.signal(Event.any(onDidRemoveLM, onDidChangeModelAccess)); - }, - makeChatRequest(messages, options, token) { - if (that._isUsingAuth(from, metadata) && !that._modelAccessList.get(from)?.has(metadata.extension)) { - throw new Error('Access to chat has been revoked'); - } - if (!that._languageModelIds.has(languageModelId)) { - throw new Error('Language Model has been removed'); - } - const cts = new CancellationTokenSource(token); - const requestId = (Math.random() * 1e6) | 0; - const requestPromise = that._proxy.$fetchResponse(from, languageModelId, requestId, messages.map(typeConvert.LanguageModelMessage.from), options ?? {}, cts.token); - const res = new LanguageModelRequest(requestPromise, cts); - that._pendingRequest.set(requestId, { languageModelId, res }); - - requestPromise.finally(() => { - that._pendingRequest.delete(requestId); - cts.dispose(); - }); - - return res.apiObject; - }, - }; + return res.apiObject; } async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise { @@ -317,22 +301,32 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } // BIG HACK: Using AuthenticationProviders to check access to Language Models - private async _getAuthAccess(from: IExtensionDescription, to: { identifier: ExtensionIdentifier; displayName: string }, justification?: string): Promise { + private async _getAuthAccess(from: IExtensionDescription, to: { identifier: ExtensionIdentifier; displayName: string }, justification: string | undefined, silent: boolean | undefined): Promise { // This needs to be done in both MainThread & ExtHost ChatProvider const providerId = INTERNAL_AUTH_PROVIDER_PREFIX + to.identifier.value; const session = await this._extHostAuthentication.getSession(from, providerId, [], { silent: true }); - if (!session) { - try { - const detail = justification - ? localize('chatAccessWithJustification', "To allow access to the language models provided by {0}. Justification:\n\n{1}", to.displayName, justification) - : localize('chatAccess', "To allow access to the language models provided by {0}", to.displayName); - await this._extHostAuthentication.getSession(from, providerId, [], { forceNewSession: { detail } }); - } catch (err) { - throw new Error('Access to language models has not been granted'); - } + + if (session) { + this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); + return true; + } + + if (silent) { + return false; } - this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); + try { + const detail = justification + ? localize('chatAccessWithJustification', "To allow access to the language models provided by {0}. Justification:\n\n{1}", to.displayName, justification) + : localize('chatAccess', "To allow access to the language models provided by {0}", to.displayName); + await this._extHostAuthentication.getSession(from, providerId, [], { forceNewSession: { detail } }); + this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); + return true; + + } catch (err) { + // ignore + return false; + } } private _isUsingAuth(from: ExtensionIdentifier, toMetadata: ILanguageModelChatMetadata): toMetadata is ILanguageModelChatMetadata & { auth: NonNullable } { @@ -341,4 +335,50 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { // And we're asking from a different extension && !ExtensionIdentifier.equals(toMetadata.extension, from); } + + private async _fakeAuthPopulate(metadata: ILanguageModelChatMetadata): Promise { + + for (const from of this._languageAccessInformationExtensions) { + try { + await this._getAuthAccess(from, { identifier: metadata.extension, displayName: '' }, undefined, true); + } catch (err) { + this._logService.error('Fake Auth request failed'); + this._logService.error(err); + } + } + + } + + private readonly _languageAccessInformationExtensions = new Set>(); + + createLanguageModelAccessInformation(from: Readonly): vscode.LanguageModelAccessInformation { + + this._languageAccessInformationExtensions.add(from); + + const that = this; + const _onDidChangeAccess = Event.signal(Event.filter(this._onDidChangeModelAccess.event, e => ExtensionIdentifier.equals(e.from, from.identifier))); + const _onDidAddRemove = Event.signal(this._onDidChangeProviders.event); + + return { + get onDidChange() { + return Event.any(_onDidChangeAccess, _onDidAddRemove); + }, + canSendRequest(languageModelId: string): boolean | undefined { + + const data = that._allLanguageModelData.get(languageModelId); + if (!data) { + return undefined; + } + if (!that._isUsingAuth(from.identifier, data)) { + return true; + } + + const list = that._modelAccessList.get(from.identifier); + if (!list) { + return undefined; + } + return list.has(data.extension); + } + }; + } } diff --git a/code/src/vs/workbench/api/common/extHostNotebookDocument.ts b/code/src/vs/workbench/api/common/extHostNotebookDocument.ts index 2cc7a200edc..8f74a0a4b69 100644 --- a/code/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/code/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -442,17 +442,7 @@ export class ExtHostNotebookDocument { return this._cells[index]; } - getCell(cellHandle: number | URI): ExtHostCell | undefined { - if (URI.isUri(cellHandle)) { - const data = notebookCommon.CellUri.parse(cellHandle); - if (!data) { - return undefined; - } - if (data.notebook.toString() !== this.uri.toString()) { - return undefined; - } - cellHandle = data.handle; - } + getCell(cellHandle: number): ExtHostCell | undefined { return this._cells.find(cell => cell.handle === cellHandle); } diff --git a/code/src/vs/workbench/api/common/extHostNotebookKernels.ts b/code/src/vs/workbench/api/common/extHostNotebookKernels.ts index f2a201e9e06..a174de9e95b 100644 --- a/code/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/code/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -96,13 +96,18 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { '_executeNotebookVariableProvider', 'Execute notebook variable provider', [ApiCommandArgument.Uri], - new ApiCommandResult('A promise that resolves to an array of variables', (value, apiArgs) => { + new ApiCommandResult('A promise that resolves to an array of variables', (value, apiArgs) => { return value.map(variable => { return { - name: variable.name, - value: variable.value, - type: variable.type, - editable: false + variable: { + name: variable.name, + value: variable.value, + expression: variable.expression, + type: variable.type, + language: variable.language + }, + hasNamedChildren: variable.hasNamedChildren, + indexedChildrenCount: variable.indexedChildrenCount }; }); }) @@ -475,6 +480,8 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { name: result.variable.name, value: result.variable.value, type: result.variable.type, + language: result.variable.language, + expression: result.variable.expression, hasNamedChildren: result.hasNamedChildren, indexedChildrenCount: result.indexedChildrenCount, extensionId: obj.extensionId.value, @@ -700,7 +707,7 @@ class NotebookCellExecutionTask extends Disposable { }); }, - end(success: boolean | undefined, endTime?: number): void { + end(success: boolean | undefined, endTime?: number, executionError?: vscode.CellExecutionError): void { if (that._state === NotebookCellExecutionTaskState.Resolved) { throw new Error('Cannot call resolve twice'); } @@ -712,9 +719,22 @@ class NotebookCellExecutionTask extends Disposable { // so we use updateSoon and immediately flush. that._collector.flush(); + const error = executionError ? { + message: executionError.message, + stack: executionError.stack, + location: executionError?.location ? { + startLineNumber: executionError.location.start.line, + startColumn: executionError.location.start.character, + endLineNumber: executionError.location.end.line, + endColumn: executionError.location.end.character + } : undefined, + uri: executionError.uri + } : undefined; + that._proxy.$completeExecution(that._handle, new SerializableObjectWithBuffers({ runEndTime: endTime, - lastRunSuccess: success + lastRunSuccess: success, + error })); }, diff --git a/code/src/vs/workbench/api/common/extHostSearch.ts b/code/src/vs/workbench/api/common/extHostSearch.ts index d0289669abf..c2e0b93f7b9 100644 --- a/code/src/vs/workbench/api/common/extHostSearch.ts +++ b/code/src/vs/workbench/api/common/extHostSearch.ts @@ -11,13 +11,14 @@ import { FileSearchManager } from 'vs/workbench/services/search/common/fileSearc import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { ILogService } from 'vs/platform/log/common/log'; -import { IRawFileQuery, ISearchCompleteStats, IFileQuery, IRawTextQuery, IRawQuery, ITextQuery, IFolderQuery } from 'vs/workbench/services/search/common/search'; +import { IRawFileQuery, ISearchCompleteStats, IFileQuery, IRawTextQuery, IRawQuery, ITextQuery, IFolderQuery, IRawAITextQuery, IAITextQuery } from 'vs/workbench/services/search/common/search'; import { URI, UriComponents } from 'vs/base/common/uri'; import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; import { CancellationToken } from 'vs/base/common/cancellation'; export interface IExtHostSearch extends ExtHostSearchShape { registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProvider): IDisposable; + registerAITextSearchProvider(scheme: string, provider: vscode.AITextSearchProvider): IDisposable; registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProvider): IDisposable; doInternalFileSearchWithCustomCallback(query: IFileQuery, token: CancellationToken, handleFileMatch: (data: URI[]) => void): Promise; } @@ -31,6 +32,10 @@ export class ExtHostSearch implements ExtHostSearchShape { private readonly _textSearchProvider = new Map(); private readonly _textSearchUsedSchemes = new Set(); + + private readonly _aiTextSearchProvider = new Map(); + private readonly _aiTextSearchUsedSchemes = new Set(); + private readonly _fileSearchProvider = new Map(); private readonly _fileSearchUsedSchemes = new Set(); @@ -62,6 +67,22 @@ export class ExtHostSearch implements ExtHostSearchShape { }); } + registerAITextSearchProvider(scheme: string, provider: vscode.AITextSearchProvider): IDisposable { + if (this._aiTextSearchUsedSchemes.has(scheme)) { + throw new Error(`an AI text search provider for the scheme '${scheme}'is already registered`); + } + + this._aiTextSearchUsedSchemes.add(scheme); + const handle = this._handlePool++; + this._aiTextSearchProvider.set(handle, provider); + this._proxy.$registerAITextSearchProvider(handle, this._transformScheme(scheme)); + return toDisposable(() => { + this._aiTextSearchUsedSchemes.delete(scheme); + this._aiTextSearchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); + } + registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProvider): IDisposable { if (this._fileSearchUsedSchemes.has(scheme)) { throw new Error(`a file search provider for the scheme '${scheme}' is already registered`); @@ -86,7 +107,7 @@ export class ExtHostSearch implements ExtHostSearchShape { this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource)); }, token); } else { - throw new Error('unknown provider: ' + handle); + throw new Error('3 unknown provider: ' + handle); } } @@ -103,7 +124,7 @@ export class ExtHostSearch implements ExtHostSearchShape { $provideTextSearchResults(handle: number, session: number, rawQuery: IRawTextQuery, token: vscode.CancellationToken): Promise { const provider = this._textSearchProvider.get(handle); if (!provider || !provider.provideTextSearchResults) { - throw new Error(`Unknown provider ${handle}`); + throw new Error(`2 Unknown provider ${handle}`); } const query = reviveQuery(rawQuery); @@ -111,17 +132,35 @@ export class ExtHostSearch implements ExtHostSearchShape { return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token); } + $provideAITextSearchResults(handle: number, session: number, rawQuery: IRawAITextQuery, token: vscode.CancellationToken): Promise { + const provider = this._aiTextSearchProvider.get(handle); + if (!provider || !provider.provideAITextSearchResults) { + throw new Error(`1 Unknown provider ${handle}`); + } + + const query = reviveQuery(rawQuery); + const engine = this.createAITextSearchManager(query, provider); + return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token); + } + $enableExtensionHostSearch(): void { } protected createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider): TextSearchManager { - return new TextSearchManager(query, provider, { - readdir: resource => Promise.resolve([]), // TODO@rob implement + return new TextSearchManager({ query, provider }, { + readdir: resource => Promise.resolve([]), toCanonicalName: encoding => encoding }, 'textSearchProvider'); } + + protected createAITextSearchManager(query: IAITextQuery, provider: vscode.AITextSearchProvider): TextSearchManager { + return new TextSearchManager({ query, provider }, { + readdir: resource => Promise.resolve([]), + toCanonicalName: encoding => encoding + }, 'aiTextSearchProvider'); + } } -export function reviveQuery(rawQuery: U): U extends IRawTextQuery ? ITextQuery : IFileQuery { +export function reviveQuery(rawQuery: U): U extends IRawTextQuery ? ITextQuery : U extends IRawAITextQuery ? IAITextQuery : IFileQuery { return { ...rawQuery, // TODO@rob ??? ...{ diff --git a/code/src/vs/workbench/api/common/extHostTesting.ts b/code/src/vs/workbench/api/common/extHostTesting.ts index 597865e61c0..c15138fe33b 100644 --- a/code/src/vs/workbench/api/common/extHostTesting.ts +++ b/code/src/vs/workbench/api/common/extHostTesting.ts @@ -13,7 +13,6 @@ import { createSingleCallFunction } from 'vs/base/common/functional'; import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { deepFreeze } from 'vs/base/common/objects'; import { isDefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -28,7 +27,7 @@ import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extH import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, KEEP_N_LAST_COVERAGE_REPORTS, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; @@ -74,9 +73,11 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return controller?.collection.tree.get(targetTest)?.actual ?? toItemFromContext(arg); } case MarshalledId.TestMessageMenuArgs: { - const { extId, message } = arg as ITestMessageMenuArgs; + const { test, message } = arg as ITestMessageMenuArgs; + const extId = test.item.extId; return { - test: this.controllers.get(TestId.root(extId))?.collection.tree.get(extId)?.actual, + test: this.controllers.get(TestId.root(extId))?.collection.tree.get(extId)?.actual + ?? toItemFromContext({ $mid: MarshalledId.TestItemContext, tests: [test] }), message: Convert.TestMessage.to(message as ITestErrorMessage.Serialized), }; } @@ -235,19 +236,16 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { /** * @inheritdoc */ - async $provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise { - const coverage = this.runTracker.getCoverageReport(runId, taskId); - const fileCoverage = await coverage?.provideFileCoverage(token); - return fileCoverage ?? []; + async $getCoverageDetails(coverageId: string, token: CancellationToken): Promise { + const details = await this.runTracker.getCoverageDetails(coverageId, token); + return details?.map(Convert.TestCoverage.fromDetails); } /** * @inheritdoc */ - async $resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise { - const coverage = this.runTracker.getCoverageReport(runId, taskId); - const details = await coverage?.resolveFileCoverage(fileIndex, token); - return details ?? []; + async $disposeRun(runId: string) { + this.runTracker.disposeTestRun(runId); } /** @inheritdoc */ @@ -294,7 +292,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { public $publishTestResults(results: ISerializedTestResults[]): void { this.results = Object.freeze( results - .map(r => deepFreeze(Convert.TestResults.to(r))) + .map(Convert.TestResults.to) .concat(this.results) .sort((a, b) => b.completedAt - a.completedAt) .slice(0, 32), @@ -388,6 +386,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { publicReq, TestRunDto.fromInternal(req, lookup.collection), extension, + profile, token, ); @@ -401,8 +400,6 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { if (tracker.hasRunningTasks && !token.isCancellationRequested) { await Event.toPromise(tracker.onEnd); } - - tracker.dispose(); } } } @@ -433,16 +430,16 @@ const enum TestRunTrackerState { class TestRunTracker extends Disposable { private state = TestRunTrackerState.Running; + private running = 0; private readonly tasks = new Map(); private readonly sharedTestIds = new Set(); private readonly cts: CancellationTokenSource; private readonly endEmitter = this._register(new Emitter()); - private readonly coverageEmitter = this._register(new Emitter<{ runId: string; taskId: string; coverage: TestRunCoverageBearer | undefined }>()); - - /** - * Fired when a coverage provider is added or removed from a task. - */ - public readonly onDidCoverage = this.coverageEmitter.event; + private readonly onDidDispose: Event; + private readonly publishedCoverage = new Map Thenable; + }>(); /** * Fires when a test ends, and no more tests are left running. @@ -453,7 +450,7 @@ class TestRunTracker extends Disposable { * Gets whether there are any tests running. */ public get hasRunningTasks() { - return this.tasks.size > 0; + return this.running > 0; } /** @@ -468,6 +465,7 @@ class TestRunTracker extends Disposable { private readonly proxy: MainThreadTestingShape, private readonly extension: IRelaxedExtensionDescription, private readonly logService: ILogService, + private readonly profile: vscode.TestRunProfile | undefined, parentToken?: CancellationToken, ) { super(); @@ -475,6 +473,13 @@ class TestRunTracker extends Disposable { const forciblyEnd = this._register(new RunOnceScheduler(() => this.forciblyEndTasks(), RUN_CANCEL_DEADLINE)); this._register(this.cts.token.onCancellationRequested(() => forciblyEnd.schedule())); + + const didDisposeEmitter = new Emitter(); + this.onDidDispose = didDisposeEmitter.event; + this._register(toDisposable(() => { + didDisposeEmitter.fire(); + didDisposeEmitter.dispose(); + })); } /** Requests cancellation of the run. On the second call, forces cancellation. */ @@ -487,14 +492,32 @@ class TestRunTracker extends Disposable { } } + /** Gets details for a previously-emitted coverage object. */ + public getCoverageDetails(id: string, token: CancellationToken) { + const [, taskId, covId] = TestId.fromString(id).path; /** runId, taskId, URI */ + const obj = this.publishedCoverage.get(covId); + if (!obj) { + return []; + } + + if (obj.backCompatResolve) { + return obj.backCompatResolve(token); + } + + const task = this.tasks.get(taskId); + if (!task) { + throw new Error('unreachable: run task was not found'); + } + + return this.profile?.loadDetailedCoverage?.(task.run, obj.coverage, token) ?? []; + } + /** Creates the public test run interface to give to extensions. */ public createRun(name: string | undefined): vscode.TestRun { const runId = this.dto.id; const ctrlId = this.dto.controllerId; const taskId = generateUuid(); const extension = this.extension; - const coverageEmitter = this.coverageEmitter; - let coverage: TestRunCoverageBearer | undefined; const guardTestMutation = (fn: (test: vscode.TestItem, ...args: Args) => void) => (test: vscode.TestItem, ...args: Args) => { @@ -526,18 +549,45 @@ class TestRunTracker extends Disposable { this.proxy.$appendTestMessagesInRun(runId, taskId, TestId.fromExtHostTestItem(test, ctrlId).toString(), converted); }; + const addCoverage = (coverage: vscode.FileCoverage, backCompatResolve?: (token: vscode.CancellationToken) => Thenable) => { + const uriStr = coverage.uri.toString(); + const id = new TestId([runId, taskId, uriStr]).toString(); + this.publishedCoverage.set(uriStr, { coverage, backCompatResolve }); + this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(id, coverage)); + }; + + interface ICoverageProvider { + provideFileCoverage(token: CancellationToken): vscode.ProviderResult; + resolveFileCoverage?(coverage: vscode.FileCoverage, token: CancellationToken): vscode.ProviderResult; + } + let ended = false; - const run: vscode.TestRun = { + let coverageProvider: ICoverageProvider | undefined; + const run: vscode.TestRun & { coverageProvider?: ICoverageProvider } = { isPersisted: this.dto.isPersisted, token: this.cts.token, name, + onDidDispose: this.onDidDispose, + // todo@connor4312: back compat get coverageProvider() { - return coverage?.provider; + return coverageProvider; }, - set coverageProvider(provider) { + // todo@connor4312: back compat + set coverageProvider(provider: ICoverageProvider | undefined) { checkProposedApiEnabled(extension, 'testCoverage'); - coverage = provider && new TestRunCoverageBearer(provider); - coverageEmitter.fire({ taskId, runId, coverage }); + coverageProvider = provider; + if (provider) { + Promise.resolve(provider.provideFileCoverage(CancellationToken.None)).then(coverage => { + coverage?.forEach(c => addCoverage(c, provider.resolveFileCoverage && (async token => { + const r = await provider.resolveFileCoverage!(c, token); + return (r || c as any).detailedCoverage; + }))); + }); + } + }, + addCoverage: coverage => { + checkProposedApiEnabled(extension, 'testCoverage'); + addCoverage(coverage); }, //#region state mutation enqueued: guardTestMutation(test => { @@ -589,13 +639,13 @@ class TestRunTracker extends Disposable { ended = true; this.proxy.$finishedTestRunTask(runId, taskId); - this.tasks.delete(taskId); - if (!this.tasks.size) { + if (!--this.running) { this.markEnded(); } } }; + this.running++; this.tasks.set(taskId, { run }); this.proxy.$startedTestRunTask(runId, { id: taskId, name, running: true }); @@ -651,18 +701,13 @@ class TestRunTracker extends Disposable { } } -interface CoverageReportRecord { - runId: string; - coverage: Map; -} - /** * Queues runs for a single extension and provides the currently-executing * run so that `createTestRun` can be properly correlated. */ export class TestRunCoordinator { private readonly tracked = new Map(); - private readonly coverageReports: CoverageReportRecord[] = []; + private readonly trackedById = new Map(); public get trackers() { return this.tracked.values(); @@ -676,10 +721,23 @@ export class TestRunCoordinator { /** * Gets a coverage report for a given run and task ID. */ - public getCoverageReport(runId: string, taskId: string) { - return this.coverageReports - .find(r => r.runId === runId) - ?.coverage.get(taskId); + public getCoverageDetails(id: string, token: vscode.CancellationToken) { + const runId = TestId.root(id); + return this.trackedById.get(runId)?.getCoverageDetails(id, token) || []; + } + + /** + * Disposes the test run, called when the main thread is no longer interested + * in associated data. + */ + public disposeTestRun(runId: string) { + this.trackedById.get(runId)?.dispose(); + this.trackedById.delete(runId); + for (const [req, { id }] of this.tracked) { + if (id === runId) { + this.tracked.delete(req); + } + } } /** @@ -687,20 +745,15 @@ export class TestRunCoordinator { * `$startedExtensionTestRun` is not invoked. The run must eventually * be cancelled manually. */ - public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, extension: Readonly, token: CancellationToken) { - return this.getTracker(req, dto, extension, token); + public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, extension: Readonly, profile: vscode.TestRunProfile, token: CancellationToken) { + return this.getTracker(req, dto, extension, profile, token); } /** * Cancels an existing test run via its cancellation token. */ public cancelRunById(runId: string) { - for (const tracker of this.tracked.values()) { - if (tracker.id === runId) { - tracker.cancel(); - return; - } - } + this.trackedById.get(runId)?.cancel(); } /** @@ -712,7 +765,6 @@ export class TestRunCoordinator { } } - /** * Implements the public `createTestRun` API. */ @@ -736,37 +788,18 @@ export class TestRunCoordinator { persist }); - const tracker = this.getTracker(request, dto, extension); + const tracker = this.getTracker(request, dto, extension, request.profile); Event.once(tracker.onEnd)(() => { this.proxy.$finishedExtensionTestRun(dto.id); - tracker.dispose(); }); return tracker.createRun(name); } - private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, extension: IRelaxedExtensionDescription, token?: CancellationToken) { - const tracker = new TestRunTracker(dto, this.proxy, extension, this.logService, token); + private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, extension: IRelaxedExtensionDescription, profile: vscode.TestRunProfile | undefined, token?: CancellationToken) { + const tracker = new TestRunTracker(dto, this.proxy, extension, this.logService, profile, token); this.tracked.set(req, tracker); - - let coverageReports: CoverageReportRecord | undefined; - const coverageListener = tracker.onDidCoverage(({ runId, taskId, coverage }) => { - if (!coverageReports) { - coverageReports = { runId, coverage: new Map() }; - this.coverageReports.unshift(coverageReports); - if (this.coverageReports.length > KEEP_N_LAST_COVERAGE_REPORTS) { - this.coverageReports.pop(); - } - } - - coverageReports.coverage.set(taskId, coverage); - this.proxy.$signalCoverageAvailable(runId, taskId, !!coverage); - }); - - Event.once(tracker.onEnd)(() => { - this.tracked.delete(req); - coverageListener.dispose(); - }); + this.trackedById.set(tracker.id, tracker); return tracker; } } @@ -839,40 +872,6 @@ export class TestRunDto { } } -class TestRunCoverageBearer { - private fileCoverage?: Promise; - - constructor(public readonly provider: vscode.TestCoverageProvider) { } - - public async provideFileCoverage(token: CancellationToken): Promise { - if (!this.fileCoverage) { - this.fileCoverage = (async () => this.provider.provideFileCoverage(token))(); - } - - try { - const coverage = await this.fileCoverage; - return coverage?.map(Convert.TestCoverage.fromFile) ?? []; - } catch (e) { - this.fileCoverage = undefined; - throw e; - } - } - - public async resolveFileCoverage(index: number, token: CancellationToken): Promise { - const fileCoverage = await this.fileCoverage; - let file = fileCoverage?.[index]; - if (!this.provider || !fileCoverage || !file) { - return []; - } - - if (!file.detailedCoverage) { - file = fileCoverage[index] = await this.provider.resolveFileCoverage?.(file, token) ?? file; - } - - return file.detailedCoverage?.map(Convert.TestCoverage.fromDetailed) ?? []; - } -} - /** * @private */ diff --git a/code/src/vs/workbench/api/common/extHostTunnelService.ts b/code/src/vs/workbench/api/common/extHostTunnelService.ts index ccf5700c83b..650b852e1e4 100644 --- a/code/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/code/src/vs/workbench/api/common/extHostTunnelService.ts @@ -3,7 +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 { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; @@ -103,6 +103,9 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe } registerPortsAttributesProvider(portSelector: PortAttributesSelector, provider: vscode.PortAttributesProvider): vscode.Disposable { + if (portSelector.portRange === undefined && portSelector.commandPattern === undefined) { + this.logService.error('PortAttributesProvider must specify either a portRange or a commandPattern'); + } const providerHandle = this.nextPortAttributesProviderHandle(); this._portAttributesProviders.set(providerHandle, { selector: portSelector, provider }); @@ -149,7 +152,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe throw new Error('A tunnel provider has already been registered. Only the first tunnel provider to be registered will be used.'); } this._forwardPortProvider = async (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => { - const result = await provider.provideTunnel(tunnelOptions, tunnelCreationOptions, new CancellationTokenSource().token); + const result = await provider.provideTunnel(tunnelOptions, tunnelCreationOptions, CancellationToken.None); return result ?? undefined; }; diff --git a/code/src/vs/workbench/api/common/extHostTypeConverters.ts b/code/src/vs/workbench/api/common/extHostTypeConverters.ts index 816f0227361..c997c0fb383 100644 --- a/code/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/code/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -14,12 +14,12 @@ import { marked } from 'vs/base/common/marked/marked'; import { parse, revive } from 'vs/base/common/marshalling'; import { Mimes } from 'vs/base/common/mime'; import { cloneAndChange } from 'vs/base/common/objects'; +import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { basename } from 'vs/base/common/resources'; -import { isEmptyObject, isNumber, isString, isUndefinedOrNull } from 'vs/base/common/types'; +import { isDefined, isEmptyObject, isNumber, isString, isUndefinedOrNull } from 'vs/base/common/types'; import { URI, UriComponents, isUriComponents } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; -import { IOffsetRange } from 'vs/editor/common/core/offsetRange'; import { IPosition } from 'vs/editor/common/core/position'; import * as editorRange from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; @@ -37,16 +37,17 @@ import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/editor'; import { IViewBadge } from 'vs/workbench/common/views'; -import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; +import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTreeData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; +import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import { IInlineChatCommandFollowup, IInlineChatFollowup, IInlineChatReplyFollowup, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestTag, TestMessageType, TestResultItem, denamespaceTestTag, namespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -1956,18 +1957,15 @@ export namespace TestTag { } export namespace TestResults { - const convertTestResultItem = (item: TestResultItem.Serialized, byInternalId: Map): vscode.TestResultSnapshot => { - const children: TestResultItem.Serialized[] = []; - for (const [id, item] of byInternalId) { - if (TestId.compare(item.item.extId, id) === TestPosition.IsChild) { - byInternalId.delete(id); - children.push(item); - } + const convertTestResultItem = (node: IPrefixTreeNode, parent?: vscode.TestResultSnapshot): vscode.TestResultSnapshot | undefined => { + const item = node.value; + if (!item) { + return undefined; // should be unreachable } const snapshot: vscode.TestResultSnapshot = ({ ...TestItem.toPlain(item.item), - parent: undefined, + parent, taskStates: item.tasks.map(t => ({ state: t.state as number as types.TestResultState, duration: t.duration, @@ -1975,30 +1973,43 @@ export namespace TestResults { .filter((m): m is ITestErrorMessage.Serialized => m.type === TestMessageType.Error) .map(TestMessage.to), })), - children: children.map(c => convertTestResultItem(c, byInternalId)) + children: [], }); - for (const child of snapshot.children) { - (child as any).parent = snapshot; + if (node.children) { + for (const child of node.children.values()) { + const c = convertTestResultItem(child, snapshot); + if (c) { + snapshot.children.push(c); + } + } } return snapshot; }; export function to(serialized: ISerializedTestResults): vscode.TestRunResult { - const roots: TestResultItem.Serialized[] = []; - const byInternalId = new Map(); + const tree = new WellDefinedPrefixTree(); for (const item of serialized.items) { - byInternalId.set(item.item.extId, item); - const controllerId = TestId.root(item.item.extId); - if (serialized.request.targets.some(t => t.controllerId === controllerId && t.testIds.includes(item.item.extId))) { - roots.push(item); + tree.insert(TestId.fromString(item.item.extId).path, item); + } + + // Get the first node with a value in each subtree of IDs. + const queue = [tree.nodes]; + const roots: IPrefixTreeNode[] = []; + while (queue.length) { + for (const node of queue.pop()!) { + if (node.value) { + roots.push(node); + } else if (node.children) { + queue.push(node.children.values()); + } } } return { completedAt: serialized.completedAt, - results: roots.map(r => convertTestResultItem(r, byInternalId)), + results: roots.map(r => convertTestResultItem(r)).filter(isDefined), }; } } @@ -2012,7 +2023,7 @@ export namespace TestCoverage { return 'line' in location ? Position.from(location) : Range.from(location); } - export function fromDetailed(coverage: vscode.DetailedCoverage): CoverageDetails.Serialized { + export function fromDetails(coverage: vscode.FileCoverageDetail): CoverageDetails.Serialized { if ('branches' in coverage) { return { count: coverage.executed, @@ -2032,13 +2043,13 @@ export namespace TestCoverage { } } - export function fromFile(coverage: vscode.FileCoverage): IFileCoverage.Serialized { + export function fromFile(id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { return { + id, uri: coverage.uri, statement: fromCoveredCount(coverage.statementCoverage), branch: coverage.branchCoverage && fromCoveredCount(coverage.branchCoverage), declaration: coverage.declarationCoverage && fromCoveredCount(coverage.declarationCoverage), - details: coverage.detailedCoverage?.map(fromDetailed), }; } } @@ -2239,20 +2250,20 @@ export namespace ChatInlineFollowup { export namespace LanguageModelMessage { - export function to(message: chatProvider.IChatMessage): vscode.LanguageModelMessage { + export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelSystemMessage(message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelUserMessage(message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelAssistantMessage(message.content); + case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatSystemMessage(message.content); + case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatUserMessage(message.content); + case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatAssistantMessage(message.content); } } - export function from(message: vscode.LanguageModelMessage): chatProvider.IChatMessage { - if (message instanceof types.LanguageModelSystemMessage) { + export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { + if (message instanceof types.LanguageModelChatSystemMessage) { return { role: chatProvider.ChatMessageRole.System, content: message.content }; - } else if (message instanceof types.LanguageModelUserMessage) { + } else if (message instanceof types.LanguageModelChatUserMessage) { return { role: chatProvider.ChatMessageRole.User, content: message.content }; - } else if (message instanceof types.LanguageModelAssistantMessage) { + } else if (message instanceof types.LanguageModelChatAssistantMessage) { return { role: chatProvider.ChatMessageRole.Assistant, content: message.content }; } else { throw new Error('Invalid LanguageModelMessage'); @@ -2603,16 +2614,27 @@ export namespace ChatAgentRequest { return { prompt: request.message, command: request.command, - variables: request.variables.variables.map(ChatAgentResolvedVariable.to) + variables: request.variables.variables.map(ChatAgentResolvedVariable.to), + location: ChatLocation.to(request.location), }; } } +export namespace ChatLocation { + export function to(loc: ChatAgentLocation): types.ChatLocation { + switch (loc) { + case ChatAgentLocation.Notebook: return types.ChatLocation.Notebook; + case ChatAgentLocation.Terminal: return types.ChatLocation.Terminal; + case ChatAgentLocation.Panel: return types.ChatLocation.Panel; + } + } +} + export namespace ChatAgentResolvedVariable { - export function to(request: { name: string; range: IOffsetRange; values: IChatRequestVariableValue[] }): vscode.ChatResolvedVariable { + export function to(request: IChatRequestVariableEntry): vscode.ChatResolvedVariable { return { name: request.name, - range: [request.range.start, request.range.endExclusive], + range: request.range && [request.range.start, request.range.endExclusive], values: request.values.map(ChatVariable.to) }; } @@ -2672,6 +2694,29 @@ export namespace TerminalQuickFix { } } +export namespace PartialAcceptInfo { + export function to(info: languages.PartialAcceptInfo): types.PartialAcceptInfo { + return { + kind: PartialAcceptTriggerKind.to(info.kind), + }; + } +} + +export namespace PartialAcceptTriggerKind { + export function to(kind: languages.PartialAcceptTriggerKind): types.PartialAcceptTriggerKind { + switch (kind) { + case languages.PartialAcceptTriggerKind.Word: + return types.PartialAcceptTriggerKind.Word; + case languages.PartialAcceptTriggerKind.Line: + return types.PartialAcceptTriggerKind.Line; + case languages.PartialAcceptTriggerKind.Suggest: + return types.PartialAcceptTriggerKind.Suggest; + default: + return types.PartialAcceptTriggerKind.Unknown; + } + } +} + export namespace DebugTreeItem { export function from(item: vscode.DebugTreeItem, id: number): IDebugVisualizationTreeItem { return { diff --git a/code/src/vs/workbench/api/common/extHostTypes.ts b/code/src/vs/workbench/api/common/extHostTypes.ts index 4fb94e3f1c8..97f29a8834f 100644 --- a/code/src/vs/workbench/api/common/extHostTypes.ts +++ b/code/src/vs/workbench/api/common/extHostTypes.ts @@ -1795,6 +1795,17 @@ export class InlineSuggestionList implements vscode.InlineCompletionList { } } +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, +} + export enum ViewColumn { Active = -1, Beside = -2, @@ -2775,27 +2786,63 @@ export class DataTransfer implements vscode.DataTransfer { @es5ClassCompat export class DocumentDropEdit { + title?: string; + id: string | undefined; insertText: string | SnippetString; additionalEdit?: WorkspaceEdit; - constructor(insertText: string | SnippetString) { + kind?: DocumentPasteEditKind; + + constructor(insertText: string | SnippetString, title?: string, kind?: DocumentPasteEditKind) { this.insertText = insertText; + this.title = title; + this.kind = kind; + } +} + +export enum DocumentPasteTriggerKind { + Automatic = 0, + PasteAs = 1, +} + +export class DocumentPasteEditKind { + static Empty: DocumentPasteEditKind; + + private static sep = '.'; + + constructor( + public readonly value: string + ) { } + + public append(...parts: string[]): DocumentPasteEditKind { + return new DocumentPasteEditKind((this.value ? [this.value, ...parts] : parts).join(DocumentPasteEditKind.sep)); + } + + public intersects(other: DocumentPasteEditKind): boolean { + return this.contains(other) || other.contains(this); + } + + public contains(other: DocumentPasteEditKind): boolean { + return this.value === other.value || other.value.startsWith(this.value + DocumentPasteEditKind.sep); } } +DocumentPasteEditKind.Empty = new DocumentPasteEditKind(''); @es5ClassCompat export class DocumentPasteEdit { - label: string; + title: string; insertText: string | SnippetString; additionalEdit?: WorkspaceEdit; + kind: DocumentPasteEditKind; - constructor(insertText: string | SnippetString, label: string) { - this.label = label; + constructor(insertText: string | SnippetString, title: string, kind: DocumentPasteEditKind) { + this.title = title; this.insertText = insertText; + this.kind = kind; } } @@ -3024,23 +3071,20 @@ export class DebugAdapterInlineImplementation implements vscode.DebugAdapterInli } -@es5ClassCompat -export class StackFrameFocus { +export class StackFrame implements vscode.StackFrame { constructor( public readonly session: vscode.DebugSession, - readonly threadId?: number, - readonly frameId?: number) { } + readonly threadId: number, + readonly frameId: number) { } } -@es5ClassCompat -export class ThreadFocus { +export class Thread implements vscode.Thread { constructor( public readonly session: vscode.DebugSession, - readonly threadId?: number) { } + readonly threadId: number) { } } - @es5ClassCompat export class EvaluatableExpression implements vscode.EvaluatableExpression { readonly range: vscode.Range; @@ -3228,6 +3272,11 @@ export enum CommentThreadState { Resolved = 1 } +export enum CommentThreadApplicability { + Current = 0, + Outdated = 1 +} + //#endregion //#region Semantic Coloring @@ -3988,7 +4037,7 @@ const validateCC = (cc?: vscode.CoveredCount) => { }; export class FileCoverage implements vscode.FileCoverage { - public static fromDetails(uri: vscode.Uri, details: vscode.DetailedCoverage[]): vscode.FileCoverage { + public static fromDetails(uri: vscode.Uri, details: vscode.FileCoverageDetail[]): vscode.FileCoverage { const statements = new CoveredCount(0, 0); const branches = new CoveredCount(0, 0); const decl = new CoveredCount(0, 0); @@ -4020,7 +4069,7 @@ export class FileCoverage implements vscode.FileCoverage { return coverage; } - detailedCoverage?: vscode.DetailedCoverage[]; + detailedCoverage?: vscode.FileCoverageDetail[]; constructor( public readonly uri: vscode.Uri, @@ -4155,6 +4204,10 @@ export class InteractiveWindowInput { export class ChatEditorTabInput { constructor(readonly providerId: string) { } } + +export class TextMultiDiffTabInput { + constructor(readonly textDiffs: TextDiffTabInput[]) { } +} //#endregion //#region Chat @@ -4271,7 +4324,13 @@ export class ChatResponseTurn implements vscode.ChatResponseTurn { ) { } } -export class LanguageModelSystemMessage { +export enum ChatLocation { + Panel = 1, + Terminal = 2, + Notebook = 3 +} + +export class LanguageModelChatSystemMessage { content: string; constructor(content: string) { @@ -4279,7 +4338,7 @@ export class LanguageModelSystemMessage { } } -export class LanguageModelUserMessage { +export class LanguageModelChatUserMessage { content: string; name: string | undefined; @@ -4289,7 +4348,7 @@ export class LanguageModelUserMessage { } } -export class LanguageModelAssistantMessage { +export class LanguageModelChatAssistantMessage { content: string; constructor(content: string) { @@ -4297,6 +4356,26 @@ export class LanguageModelAssistantMessage { } } +export class LanguageModelError extends Error { + + static NotFound(message?: string): LanguageModelError { + return new LanguageModelError(message, LanguageModelError.NotFound.name); + } + + static NoPermissions(message?: string): LanguageModelError { + return new LanguageModelError(message, LanguageModelError.NoPermissions.name); + } + + readonly code: string; + + constructor(message?: string, code?: string, cause?: Error) { + super(message, { cause }); + this.name = 'LanguageModelError'; + this.code = code ?? ''; + } + +} + //#endregion //#region ai diff --git a/code/src/vs/workbench/api/node/extHostDebugService.ts b/code/src/vs/workbench/api/node/extHostDebugService.ts index 90b977aa644..995d4f8a17b 100644 --- a/code/src/vs/workbench/api/node/extHostDebugService.ts +++ b/code/src/vs/workbench/api/node/extHostDebugService.ts @@ -27,6 +27,7 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import type * as vscode from 'vscode'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { createHash } from 'crypto'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -89,8 +90,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { const terminalName = args.title || nls.localize('debug.terminal.title', "Debug Process"); - const shellConfig = JSON.stringify({ shell, shellArgs }); - let terminal = await this._integratedTerminalInstances.checkout(shellConfig, terminalName); + const termKey = createKeyForShell(shell, shellArgs, args); + let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName, true); let cwdForPrepareCommand: string | undefined; let giveShellTimeToInitialize = false; @@ -102,6 +103,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { cwd: args.cwd, name: terminalName, iconPath: new ThemeIcon('debug'), + env: args.env, }; giveShellTimeToInitialize = true; terminal = this._terminalService.createTerminalFromOptions(options, { @@ -111,7 +113,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { forceShellIntegration: true, useShellEnvironment: true }); - this._integratedTerminalInstances.insert(terminal, shellConfig); + this._integratedTerminalInstances.insert(terminal, termKey); } else { cwdForPrepareCommand = args.cwd; @@ -125,6 +127,10 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { // give a new terminal some time to initialize the shell await new Promise(resolve => setTimeout(resolve, 1000)); } else { + if (terminal.state.isInteractedWith) { + terminal.sendText('\u0003'); // Ctrl+C for #106743. Not part of the same command for #107969 + } + if (configProvider.getConfiguration('debug.terminal').get('clearBeforeReusing')) { // clear terminal before reusing it if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0 || shell.indexOf('cmd.exe') >= 0) { @@ -139,7 +145,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } } - const command = prepareCommand(shell, args.args, !!args.argsCanBeInterpretedByShell, cwdForPrepareCommand, args.env); + const command = prepareCommand(shell, args.args, !!args.argsCanBeInterpretedByShell, cwdForPrepareCommand); terminal.sendText(command); // Mark terminal as unused when its session ends, see #112055 @@ -159,6 +165,14 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } } +/** Creates a key that determines how terminals get reused */ +function createKeyForShell(shell: string, shellArgs: string | string[], args: DebugProtocol.RunInTerminalRequestArguments) { + const hash = createHash('sha256'); + hash.update(JSON.stringify({ shell, shellArgs })); + hash.update(JSON.stringify(Object.entries(args.env || {}).sort(([k1], [k2]) => k1.localeCompare(k2)))); + return hash.digest('base64'); +} + let externalTerminalService: IExternalTerminalService | undefined = undefined; function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise { @@ -185,7 +199,7 @@ class DebugTerminalCollection { private _terminalInstances = new Map(); - public async checkout(config: string, name: string) { + public async checkout(config: string, name: string, cleanupOthersByName = false) { const entries = [...this._terminalInstances.entries()]; const promises = entries.map(([terminal, termInfo]) => createCancelablePromise(async ct => { @@ -205,6 +219,9 @@ class DebugTerminalCollection { } if (termInfo.config !== config) { + if (cleanupOthersByName) { + terminal.dispose(); + } return null; } diff --git a/code/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/code/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index 5e4e8ed1510..dd77886bbf0 100644 --- a/code/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/code/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -19,7 +19,7 @@ import { ExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.pro import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { IActivityService } from 'vs/workbench/services/activity/common/activity'; import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationExtensionsService, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; @@ -28,6 +28,9 @@ import { TestActivityService, TestExtensionService, TestProductService, TestStor import type { AuthenticationProvider, AuthenticationSession } from 'vscode'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; +import { AuthenticationAccessService, IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationUsageService, IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationExtensionsService } from 'vs/workbench/services/authentication/browser/authenticationExtensionsService'; class AuthQuickPick { private listener: ((e: IQuickPickDidAcceptEvent) => any) | undefined; @@ -113,9 +116,12 @@ suite('ExtHostAuthentication', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService); instantiationService.stub(IProductService, TestProductService); + instantiationService.stub(IAuthenticationAccessService, instantiationService.createInstance(AuthenticationAccessService)); + instantiationService.stub(IAuthenticationUsageService, instantiationService.createInstance(AuthenticationUsageService)); const rpcProtocol = new TestRPCProtocol(); instantiationService.stub(IAuthenticationService, instantiationService.createInstance(AuthenticationService)); + instantiationService.stub(IAuthenticationExtensionsService, instantiationService.createInstance(AuthenticationExtensionsService)); rpcProtocol.set(MainContext.MainThreadAuthentication, instantiationService.createInstance(MainThreadAuthentication, rpcProtocol)); extHostAuthentication = new ExtHostAuthentication(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication); diff --git a/code/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/code/src/vs/workbench/api/test/browser/extHostTesting.test.ts index f7e03c8d80f..e931132b918 100644 --- a/code/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/code/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -605,6 +605,12 @@ suite('ExtHost Testing', () => { let dto: TestRunDto; const ext: IRelaxedExtensionDescription = {} as any; + teardown(() => { + for (const { id } of c.trackers) { + c.disposeTestRun(id); + } + }); + setup(async () => { proxy = mockObject()(); cts = new CancellationTokenSource(); @@ -631,7 +637,7 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from a main thread request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, cts.token)); + const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, configuration, cts.token)); assert.strictEqual(tracker.hasRunningTasks, false); const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); @@ -656,7 +662,7 @@ suite('ExtHost Testing', () => { test('run cancel force ends after a timeout', () => { const clock = sinon.useFakeTimers(); try { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, cts.token)); + const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, configuration, cts.token)); const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -681,7 +687,7 @@ suite('ExtHost Testing', () => { }); test('run cancel force ends on second cancellation request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, cts.token)); + const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, configuration, cts.token)); const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); diff --git a/code/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/code/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index 727c4dea880..ab38b202b6e 100644 --- a/code/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/code/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -90,7 +90,7 @@ suite('ExtHostTypeConverter', function () { const d = new extHostTypes.NotebookData([]); d.cells.push(new extHostTypes.NotebookCellData(extHostTypes.NotebookCellKind.Code, 'hello', 'fooLang')); - d.metadata = { custom: { foo: 'bar', bar: 123 } }; + d.metadata = { foo: 'bar', bar: 123 }; const dto = NotebookData.from(d); diff --git a/code/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts b/code/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts index 2cc5a637a6a..5234d7abb34 100644 --- a/code/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts +++ b/code/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; 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'; @@ -41,7 +41,7 @@ suite('MainThreadWorkspace', () => { }); const mtw = disposables.add(instantiationService.createInstance(MainThreadWorkspace, SingleProxyRPCProtocol({ $initializeWorkspace: () => { } }))); - return mtw.$startFileSearch(null, { maxResults: 10, includePattern: 'foo', disregardSearchExcludeSettings: true }, new CancellationTokenSource().token); + return mtw.$startFileSearch(null, { maxResults: 10, includePattern: 'foo', disregardSearchExcludeSettings: true }, CancellationToken.None); }); test('exclude defaults', () => { @@ -63,7 +63,7 @@ suite('MainThreadWorkspace', () => { }); const mtw = disposables.add(instantiationService.createInstance(MainThreadWorkspace, SingleProxyRPCProtocol({ $initializeWorkspace: () => { } }))); - return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', disregardSearchExcludeSettings: true }, new CancellationTokenSource().token); + return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', disregardSearchExcludeSettings: true }, CancellationToken.None); }); test('disregard excludes', () => { @@ -84,7 +84,7 @@ suite('MainThreadWorkspace', () => { }); const mtw = disposables.add(instantiationService.createInstance(MainThreadWorkspace, SingleProxyRPCProtocol({ $initializeWorkspace: () => { } }))); - return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', disregardSearchExcludeSettings: true, disregardExcludeSettings: true }, new CancellationTokenSource().token); + return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', disregardSearchExcludeSettings: true, disregardExcludeSettings: true }, CancellationToken.None); }); test('do not disregard anything if disregardExcludeSettings is true', () => { @@ -106,7 +106,7 @@ suite('MainThreadWorkspace', () => { }); const mtw = disposables.add(instantiationService.createInstance(MainThreadWorkspace, SingleProxyRPCProtocol({ $initializeWorkspace: () => { } }))); - return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', disregardExcludeSettings: true, disregardSearchExcludeSettings: false }, new CancellationTokenSource().token); + return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', disregardExcludeSettings: true, disregardSearchExcludeSettings: false }, CancellationToken.None); }); test('exclude string', () => { @@ -120,6 +120,6 @@ suite('MainThreadWorkspace', () => { }); const mtw = disposables.add(instantiationService.createInstance(MainThreadWorkspace, SingleProxyRPCProtocol({ $initializeWorkspace: () => { } }))); - return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', excludePattern: 'exclude/**', disregardSearchExcludeSettings: true }, new CancellationTokenSource().token); + return mtw.$startFileSearch(null, { maxResults: 10, includePattern: '', excludePattern: 'exclude/**', disregardSearchExcludeSettings: true }, CancellationToken.None); }); }); diff --git a/code/src/vs/workbench/api/test/node/extHostSearch.test.ts b/code/src/vs/workbench/api/test/node/extHostSearch.test.ts index 1d903c40815..1502ef3f565 100644 --- a/code/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/code/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -43,6 +43,10 @@ class MockMainThreadSearch implements MainThreadSearchShape { this.lastHandle = handle; } + $registerAITextSearchProvider(handle: number, scheme: string): void { + this.lastHandle = handle; + } + $unregisterProvider(handle: number): void { } diff --git a/code/src/vs/workbench/browser/composite.ts b/code/src/vs/workbench/browser/composite.ts index 59eba8e11ff..d56a31212ed 100644 --- a/code/src/vs/workbench/browser/composite.ts +++ b/code/src/vs/workbench/browser/composite.ts @@ -10,7 +10,7 @@ import { IComposite, ICompositeControl } from 'vs/workbench/common/composite'; import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { trackFocus, Dimension, IDomPosition, focusWindow } from 'vs/base/browser/dom'; +import { trackFocus, Dimension, IDomPosition } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertIsDefined } from 'vs/base/common/types'; @@ -36,7 +36,7 @@ export abstract class Composite extends Component implements IComposite { private readonly _onTitleAreaUpdate = this._register(new Emitter()); readonly onTitleAreaUpdate = this._onTitleAreaUpdate.event; - private _onDidFocus: Emitter | undefined; + protected _onDidFocus: Emitter | undefined; get onDidFocus(): Event { if (!this._onDidFocus) { this._onDidFocus = this.registerFocusTrackEvents().onDidFocus; @@ -45,10 +45,6 @@ export abstract class Composite extends Component implements IComposite { return this._onDidFocus.event; } - protected fireOnDidFocus(): void { - this._onDidFocus?.fire(); - } - private _onDidBlur: Emitter | undefined; get onDidBlur(): Event { if (!this._onDidBlur) { @@ -86,22 +82,16 @@ export abstract class Composite extends Component implements IComposite { protected actionRunner: IActionRunner | undefined; - private _telemetryService: ITelemetryService; - protected get telemetryService(): ITelemetryService { return this._telemetryService; } - - private visible: boolean; + private visible = false; private parent: HTMLElement | undefined; constructor( id: string, - telemetryService: ITelemetryService, + protected readonly telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService ) { super(id, themeService, storageService); - - this._telemetryService = telemetryService; - this.visible = false; } getTitle(): string | undefined { @@ -149,13 +139,7 @@ export abstract class Composite extends Component implements IComposite { * Called when this composite should receive keyboard focus. */ focus(): void { - const container = this.getContainer(); - if (container) { - // Make sure to focus the window of the container - // because it is possible that the composite is - // opened in a auxiliary window that is not focused. - focusWindow(container); - } + // Subclasses can implement } /** diff --git a/code/src/vs/workbench/browser/contextkeys.ts b/code/src/vs/workbench/browser/contextkeys.ts index a1663743d76..71ec7e27232 100644 --- a/code/src/vs/workbench/browser/contextkeys.ts +++ b/code/src/vs/workbench/browser/contextkeys.ts @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys'; -import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, MainEditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext, TitleBarStyleContext, MultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, ActiveCompareEditorOriginalWriteableContext } from 'vs/workbench/common/contextkeys'; +import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, MainEditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext, TitleBarStyleContext, MultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow, getActiveWindow } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -29,6 +29,7 @@ import { getTitleBarStyle } from 'vs/platform/window/common/window'; import { mainWindow } from 'vs/base/browser/window'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isFullscreen, onDidChangeFullscreen } from 'vs/base/browser/browser'; +import { Schemas } from 'vs/base/common/network'; export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; @@ -41,7 +42,7 @@ export class WorkbenchContextKeysHandler extends Disposable { private activeEditorAvailableEditorIds: IContextKey; private activeEditorIsReadonly: IContextKey; - private activeCompareEditorOriginalWritable: IContextKey; + private activeCompareEditorCanSwap: IContextKey; private activeEditorCanToggleReadonly: IContextKey; private activeEditorGroupEmpty: IContextKey; @@ -130,7 +131,7 @@ export class WorkbenchContextKeysHandler extends Disposable { // Editors this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); this.activeEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.contextKeyService); - this.activeCompareEditorOriginalWritable = ActiveCompareEditorOriginalWriteableContext.bindTo(this.contextKeyService); + this.activeCompareEditorCanSwap = ActiveCompareEditorCanSwapContext.bindTo(this.contextKeyService); this.activeEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.contextKeyService); this.activeEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.contextKeyService); this.activeEditorCanSplitInGroup = ActiveEditorCanSplitInGroupContext.bindTo(this.contextKeyService); @@ -318,13 +319,14 @@ export class WorkbenchContextKeysHandler extends Disposable { this.activeEditorCanSplitInGroup.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.CanSplitInGroup)); applyAvailableEditorIds(this.activeEditorAvailableEditorIds, activeEditorPane.input, this.editorResolverService); this.activeEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); - this.activeCompareEditorOriginalWritable.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly()); const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); + const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); + this.activeCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); this.activeEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); } else { this.activeEditorContext.reset(); this.activeEditorIsReadonly.reset(); - this.activeCompareEditorOriginalWritable.reset(); + this.activeCompareEditorCanSwap.reset(); this.activeEditorCanToggleReadonly.reset(); this.activeEditorCanRevert.reset(); this.activeEditorCanSplitInGroup.reset(); diff --git a/code/src/vs/workbench/browser/editor.ts b/code/src/vs/workbench/browser/editor.ts index 7d0b333bece..d5e1a7f3391 100644 --- a/code/src/vs/workbench/browser/editor.ts +++ b/code/src/vs/workbench/browser/editor.ts @@ -59,23 +59,23 @@ export class EditorPaneDescriptor implements IEditorPaneDescriptor { static readonly onWillInstantiateEditorPane = EditorPaneDescriptor._onWillInstantiateEditorPane.event; static create( - ctor: { new(...services: Services): EditorPane }, + ctor: { new(group: IEditorGroup, ...services: Services): EditorPane }, typeId: string, name: string ): EditorPaneDescriptor { - return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); + return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); } private constructor( - private readonly ctor: IConstructorSignature, + private readonly ctor: IConstructorSignature, readonly typeId: string, readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): EditorPane { + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): EditorPane { EditorPaneDescriptor._onWillInstantiateEditorPane.fire({ typeId: this.typeId }); - const pane = instantiationService.createInstance(this.ctor); + const pane = instantiationService.createInstance(this.ctor, group); EditorPaneDescriptor.instantiatedEditorPanes.add(this.typeId); return pane; diff --git a/code/src/vs/workbench/browser/layout.ts b/code/src/vs/workbench/browser/layout.ts index 797a1713dd8..69fd77ff4ff 100644 --- a/code/src/vs/workbench/browser/layout.ts +++ b/code/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; @@ -47,7 +47,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; import { CustomTitleBarVisibility } from '../../platform/window/common/window'; //#region Layout Implementation @@ -194,6 +194,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } + private readonly containerStylesLoaded = new Map>(); + whenContainerStylesLoaded(window: CodeWindow): Promise | undefined { + return this.containerStylesLoaded.get(window.vscodeWindowId); + } + private _mainContainerDimension!: IDimension; get mainContainerDimension(): IDimension { return this._mainContainerDimension; } @@ -219,11 +224,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this.computeContainerOffset(getWindow(this.activeContainer)); } - get whenActiveContainerStylesLoaded() { - const active = this.activeContainer; - return this.auxWindowStylesLoaded.get(active) || Promise.resolve(); - } - private computeContainerOffset(targetWindow: Window) { let top = 0; let quickPickTop = 0; @@ -252,7 +252,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi //#endregion private readonly parts = new Map(); - private readonly auxWindowStylesLoaded = new Map>(); private initialized = false; private workbenchGrid!: SerializableGrid; @@ -359,9 +358,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE, ].some(setting => e.affectsConfiguration(setting))) { // Show Custom TitleBar if actions moved to the titlebar - const activityBarMovedToTop = e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION) && this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP; const editorActionsMovedToTitlebar = e.affectsConfiguration(LayoutSettings.EDITOR_ACTIONS_LOCATION) && this.configurationService.getValue(LayoutSettings.EDITOR_ACTIONS_LOCATION) === EditorActionsLocation.TITLEBAR; - if (activityBarMovedToTop || editorActionsMovedToTitlebar) { + + let activityBarMovedToTopOrBottom = false; + if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + activityBarMovedToTopOrBottom = activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM; + } + + if (activityBarMovedToTopOrBottom || editorActionsMovedToTitlebar) { if (this.configurationService.getValue(TitleBarSetting.CUSTOM_TITLE_BAR_VISIBILITY) === CustomTitleBarVisibility.NEVER) { this.configurationService.updateValue(TitleBarSetting.CUSTOM_TITLE_BAR_VISIBILITY, CustomTitleBarVisibility.AUTO); } @@ -402,12 +407,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Auxiliary windows this._register(this.auxiliaryWindowService.onDidOpenAuxiliaryWindow(({ window, disposables }) => { + const windowId = window.window.vscodeWindowId; + this.containerStylesLoaded.set(windowId, window.whenStylesHaveLoaded); + window.whenStylesHaveLoaded.then(() => this.containerStylesLoaded.delete(windowId)); + disposables.add(toDisposable(() => this.containerStylesLoaded.delete(windowId))); + const eventDisposables = disposables.add(new DisposableStore()); - this.auxWindowStylesLoaded.set(window.container, window.whenStylesHaveLoaded); this._onDidAddContainer.fire({ container: window.container, disposables: eventDisposables }); disposables.add(window.onDidLayout(dimension => this.handleContainerDidLayout(window.container, dimension))); - disposables.add(toDisposable(() => this.auxWindowStylesLoaded.delete(window.container))); })); } @@ -671,7 +679,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Only restore last viewlet if window was reloaded or we are in development mode let viewContainerToRestore: string | undefined; - if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow || isWeb) { + if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow) { viewContainerToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); } else { viewContainerToRestore = this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; @@ -1089,8 +1097,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }); } - registerPart(part: Part): void { - this.parts.set(part.getId(), part); + registerPart(part: Part): IDisposable { + const id = part.getId(); + this.parts.set(id, part); + + return toDisposable(() => this.parts.delete(id)); } protected getPart(key: Parts): Part { @@ -1124,9 +1135,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi focusPart(part: SINGLE_WINDOW_PARTS): void; focusPart(part: Parts, targetWindow: Window = mainWindow): void { const container = this.getContainer(targetWindow, part) ?? this.mainContainer; - if (container) { - focusWindow(container); - } switch (part) { case Parts.EDITOR_PART: @@ -2611,7 +2619,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === 'bottom' ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults @@ -2701,7 +2709,7 @@ class LayoutStateModel extends Disposable { if (oldValue !== undefined) { return !oldValue; } - return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.SIDE; + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.DEFAULT; } private setRuntimeValueAndFire(key: RuntimeStateKey, value: T): void { diff --git a/code/src/vs/workbench/browser/media/part.css b/code/src/vs/workbench/browser/media/part.css index 17182b6fa91..f17465a5e4a 100644 --- a/code/src/vs/workbench/browser/media/part.css +++ b/code/src/vs/workbench/browser/media/part.css @@ -22,11 +22,13 @@ z-index: 12; } -.monaco-workbench .part > .title { - display: none; /* Parts have to opt in to show title area */ +.monaco-workbench .part > .title, +.monaco-workbench .part > .header-or-footer { + display: none; /* Parts have to opt in to show area */ } -.monaco-workbench .part > .title { +.monaco-workbench .part > .title, +.monaco-workbench .part > .header-or-footer { height: 35px; display: flex; box-sizing: border-box; diff --git a/code/src/vs/workbench/browser/media/style.css b/code/src/vs/workbench/browser/media/style.css index db4862c7427..9299856d6ff 100644 --- a/code/src/vs/workbench/browser/media/style.css +++ b/code/src/vs/workbench/browser/media/style.css @@ -114,6 +114,16 @@ body.web { font-size: 100%; } +.monaco-workbench table { + /* + * Somehow this is required when tables show in floating windows + * to override the user-agent style which sets a specific color + * and font-size + */ + color: inherit; + font-size: inherit; +} + .monaco-workbench input::placeholder { color: var(--vscode-input-placeholderForeground); } .monaco-workbench input::-webkit-input-placeholder { color: var(--vscode-input-placeholderForeground); } .monaco-workbench input::-moz-placeholder { color: var(--vscode-input-placeholderForeground); } diff --git a/code/src/vs/workbench/browser/part.ts b/code/src/vs/workbench/browser/part.ts index 7ae271c65cc..086dcb51b2c 100644 --- a/code/src/vs/workbench/browser/part.ts +++ b/code/src/vs/workbench/browser/part.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/part'; import { Component } from 'vs/workbench/common/component'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; -import { Dimension, size, IDimension, getActiveDocument } from 'vs/base/browser/dom'; +import { Dimension, size, IDimension, getActiveDocument, prepend, IDomPosition } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ISerializableView, IViewSize } from 'vs/base/browser/ui/grid/grid'; import { Event, Emitter } from 'vs/base/common/event'; @@ -20,8 +20,10 @@ export interface IPartOptions { } export interface ILayoutContentResult { + readonly headerSize: IDimension; readonly titleSize: IDimension; readonly contentSize: IDimension; + readonly footerSize: IDimension; } /** @@ -33,12 +35,17 @@ export abstract class Part extends Component implements ISerializableView { private _dimension: Dimension | undefined; get dimension(): Dimension | undefined { return this._dimension; } + private _contentPosition: IDomPosition | undefined; + get contentPosition(): IDomPosition | undefined { return this._contentPosition; } + protected _onDidVisibilityChange = this._register(new Emitter()); readonly onDidVisibilityChange = this._onDidVisibilityChange.event; private parent: HTMLElement | undefined; + private headerArea: HTMLElement | undefined; private titleArea: HTMLElement | undefined; private contentArea: HTMLElement | undefined; + private footerArea: HTMLElement | undefined; private partLayout: PartLayout | undefined; constructor( @@ -50,7 +57,7 @@ export abstract class Part extends Component implements ISerializableView { ) { super(id, themeService, storageService); - layoutService.registerPart(this); + this._register(layoutService.registerPart(this)); } protected override onThemeChange(theme: IColorTheme): void { @@ -61,10 +68,6 @@ export abstract class Part extends Component implements ISerializableView { } } - override updateStyles(): void { - super.updateStyles(); - } - /** * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. @@ -116,6 +119,77 @@ export abstract class Part extends Component implements ISerializableView { return this.contentArea; } + /** + * Sets the header area + */ + protected setHeaderArea(headerContainer: HTMLElement): void { + if (this.headerArea) { + throw new Error('Header already exists'); + } + + if (!this.parent || !this.titleArea) { + return; + } + + prepend(this.parent, headerContainer); + headerContainer.classList.add('header-or-footer'); + headerContainer.classList.add('header'); + + this.headerArea = headerContainer; + this.partLayout?.setHeaderVisibility(true); + this.relayout(); + } + + /** + * Sets the footer area + */ + protected setFooterArea(footerContainer: HTMLElement): void { + if (this.footerArea) { + throw new Error('Footer already exists'); + } + + if (!this.parent || !this.titleArea) { + return; + } + + this.parent.appendChild(footerContainer); + footerContainer.classList.add('header-or-footer'); + footerContainer.classList.add('footer'); + + this.footerArea = footerContainer; + this.partLayout?.setFooterVisibility(true); + this.relayout(); + } + + /** + * removes the header area + */ + protected removeHeaderArea(): void { + if (this.headerArea) { + this.headerArea.remove(); + this.headerArea = undefined; + this.partLayout?.setHeaderVisibility(false); + this.relayout(); + } + } + + /** + * removes the footer area + */ + protected removeFooterArea(): void { + if (this.footerArea) { + this.footerArea.remove(); + this.footerArea = undefined; + this.partLayout?.setFooterVisibility(false); + this.relayout(); + } + } + + private relayout() { + if (this.dimension && this.contentPosition) { + this.layout(this.dimension.width, this.dimension.height, this.contentPosition.top, this.contentPosition.left); + } + } /** * Layout title and content area in the given dimension. */ @@ -137,8 +211,9 @@ export abstract class Part extends Component implements ISerializableView { abstract minimumHeight: number; abstract maximumHeight: number; - layout(width: number, height: number, _top: number, _left: number): void { + layout(width: number, height: number, top: number, left: number): void { this._dimension = new Dimension(width, height); + this._contentPosition = { top, left }; } setVisible(visible: boolean) { @@ -152,7 +227,12 @@ export abstract class Part extends Component implements ISerializableView { class PartLayout { + private static readonly HEADER_HEIGHT = 35; private static readonly TITLE_HEIGHT = 35; + private static readonly Footer_HEIGHT = 35; + + private headerVisible: boolean = false; + private footerVisible: boolean = false; constructor(private options: IPartOptions, private contentArea: HTMLElement | undefined) { } @@ -166,20 +246,44 @@ class PartLayout { titleSize = Dimension.None; } + // Header Size: Width (Fill), Height (Variable) + let headerSize: Dimension; + if (this.headerVisible) { + headerSize = new Dimension(width, Math.min(height, PartLayout.HEADER_HEIGHT)); + } else { + headerSize = Dimension.None; + } + + // Footer Size: Width (Fill), Height (Variable) + let footerSize: Dimension; + if (this.footerVisible) { + footerSize = new Dimension(width, Math.min(height, PartLayout.Footer_HEIGHT)); + } else { + footerSize = Dimension.None; + } + let contentWidth = width; if (this.options && typeof this.options.borderWidth === 'function') { contentWidth -= this.options.borderWidth(); // adjust for border size } // Content Size: Width (Fill), Height (Variable) - const contentSize = new Dimension(contentWidth, height - titleSize.height); + const contentSize = new Dimension(contentWidth, height - titleSize.height - headerSize.height - footerSize.height); // Content if (this.contentArea) { size(this.contentArea, contentSize.width, contentSize.height); } - return { titleSize, contentSize }; + return { headerSize, titleSize, contentSize, footerSize }; + } + + setFooterVisibility(visible: boolean): void { + this.footerVisible = visible; + } + + setHeaderVisibility(visible: boolean): void { + this.headerVisible = visible; } } @@ -197,7 +301,7 @@ export abstract class MultiWindowParts extends Compo registerPart(part: T): IDisposable { this._parts.add(part); - return this._register(toDisposable(() => this.unregisterPart(part))); + return toDisposable(() => this.unregisterPart(part)); } protected unregisterPart(part: T): void { diff --git a/code/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/code/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 2e2047e8f1d..15e58a58179 100644 --- a/code/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/code/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -378,26 +378,26 @@ export class ActivityBarCompositeBar extends PaneCompositeBar { registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.activityBarLocation.side', + id: 'workbench.action.activityBarLocation.default', title: { - ...localize2('positionActivityBarSide', 'Move Activity Bar to Side'), - mnemonicTitle: localize({ key: 'miSideActivityBar', comment: ['&& denotes a mnemonic'] }, "&&Side"), + ...localize2('positionActivityBarDefault', 'Move Activity Bar to Side'), + mnemonicTitle: localize({ key: 'miDefaultActivityBar', comment: ['&& denotes a mnemonic'] }, "&&Default"), }, - shortTitle: localize('side', "Side"), + shortTitle: localize('default', "Default"), category: Categories.View, - toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.SIDE), + toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.DEFAULT), menu: [{ id: MenuId.ActivityBarPositionMenu, order: 1 }, { id: MenuId.CommandPalette, - when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.SIDE), + when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.DEFAULT), }] }); } run(accessor: ServicesAccessor): void { const configurationService = accessor.get(IConfigurationService); - configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, ActivityBarPosition.SIDE); + configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, ActivityBarPosition.DEFAULT); } }); @@ -427,6 +427,32 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.activityBarLocation.bottom', + title: { + ...localize2('positionActivityBarBottom', 'Move Activity Bar to Bottom'), + mnemonicTitle: localize({ key: 'miBottomActivityBar', comment: ['&& denotes a mnemonic'] }, "&&Bottom"), + }, + shortTitle: localize('bottom', "Bottom"), + category: Categories.View, + toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.BOTTOM), + menu: [{ + id: MenuId.ActivityBarPositionMenu, + order: 3 + }, { + id: MenuId.CommandPalette, + when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.BOTTOM), + }] + }); + } + run(accessor: ServicesAccessor): void { + const configurationService = accessor.get(IConfigurationService); + configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, ActivityBarPosition.BOTTOM); + } +}); + registerAction2(class extends Action2 { constructor() { super({ @@ -440,7 +466,7 @@ registerAction2(class extends Action2 { toggled: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.HIDDEN), menu: [{ id: MenuId.ActivityBarPositionMenu, - order: 3 + order: 4 }, { id: MenuId.CommandPalette, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.HIDDEN), diff --git a/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 5ad1c648d6f..b22c673e7c6 100644 --- a/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -17,18 +17,24 @@ import { ActiveAuxiliaryContext, AuxiliaryBarFocusContext } from 'vs/workbench/c import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_DRAG_AND_DROP_BORDER, PANEL_INACTIVE_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_BORDER, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IAction, Separator, toAction } from 'vs/base/common/actions'; +import { IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions'; import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions'; import { assertIsDefined } from 'vs/base/common/types'; import { LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; import { ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { AbstractPaneCompositePart } from 'vs/workbench/browser/parts/paneCompositePart'; -import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { AbstractPaneCompositePart, CompositeBarPosition } from 'vs/workbench/browser/parts/paneCompositePart'; +import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPaneCompositeBarOptions } from 'vs/workbench/browser/parts/paneCompositeBar'; -import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { $ } from 'vs/base/browser/dom'; +import { HiddenItemStrategy, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { CompositeMenuActions } from 'vs/workbench/browser/actions'; export class AuxiliaryBarPart extends AbstractPaneCompositePart { @@ -79,6 +85,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { @IExtensionService extensionService: IExtensionService, @ICommandService private commandService: ICommandService, @IMenuService menuService: IMenuService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super( Parts.AUXILIARYBAR_PART, @@ -104,6 +111,21 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { extensionService, menuService, ); + + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) { + this.onDidChangeActivityBarLocation(); + } + })); + } + + private onDidChangeActivityBarLocation(): void { + this.updateCompositeBar(); + + const id = this.getActiveComposite()?.getId(); + if (id) { + this.onTitleAreaUpdate(id); + } } override updateStyles(): void { @@ -127,6 +149,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { } protected getCompositeBarOptions(): IPaneCompositeBarOptions { + const $this = this; return { partContainerClass: 'auxiliarybar', pinnedViewContainersKey: AuxiliaryBarPart.pinnedPanelsKey, @@ -136,12 +159,13 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { orientation: ActionsOrientation.HORIZONTAL, recomputeSizes: true, activityHoverOptions: { - position: () => HoverPosition.BELOW, + position: () => this.getCompositeBarPosition() === CompositeBarPosition.BOTTOM ? HoverPosition.ABOVE : HoverPosition.BELOW, }, fillExtraContextMenuActions: actions => this.fillExtraContextMenuActions(actions), compositeSize: 0, iconSize: 16, - overflowActionSize: 44, + // Add 10px spacing if the overflow action is visible to no confuse the user with ... between the toolbars + get overflowActionSize() { return $this.getCompositeBarPosition() === CompositeBarPosition.TITLE ? 40 : 30; }, colors: theme => ({ activeBackgroundColor: theme.getColor(SIDE_BAR_BACKGROUND), inactiveBackgroundColor: theme.getColor(SIDE_BAR_BACKGROUND), @@ -163,15 +187,69 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { actions.push(new Separator()); actions.push(viewsSubmenuAction); } + + const activityBarPositionMenu = this.menuService.createMenu(MenuId.ActivityBarPositionMenu, this.contextKeyService); + const positionActions: IAction[] = []; + createAndFillInContextMenuActions(activityBarPositionMenu, { shouldForwardArgs: true, renderShortTitle: true }, { primary: [], secondary: positionActions }); + activityBarPositionMenu.dispose(); + actions.push(...[ new Separator(), + new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions), toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Secondary Side Bar Left") : localize('move second side bar right', "Move Secondary Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }), toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Secondary Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) }) ]); } protected shouldShowCompositeBar(): boolean { - return true; + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.HIDDEN; + } + + protected getCompositeBarPosition(): CompositeBarPosition { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + switch (activityBarPosition) { + case ActivityBarPosition.TOP: return CompositeBarPosition.TOP; + case ActivityBarPosition.BOTTOM: return CompositeBarPosition.BOTTOM; + case ActivityBarPosition.HIDDEN: return CompositeBarPosition.TITLE; + case ActivityBarPosition.DEFAULT: return CompositeBarPosition.TITLE; + default: return CompositeBarPosition.TITLE; + } + } + + protected override createHeaderArea() { + const headerArea = super.createHeaderArea(); + const globalHeaderContainer = $('.auxiliary-bar-global-header'); + + // Add auxillary header action + const menu = this.headerFooterCompositeBarDispoables.add(this.instantiationService.createInstance(CompositeMenuActions, MenuId.AuxiliaryBarHeader, undefined, undefined)); + + const toolBar = this.headerFooterCompositeBarDispoables.add(this.instantiationService.createInstance(WorkbenchToolBar, globalHeaderContainer, { + actionViewItemProvider: (action, options) => this.headerActionViewItemProvider(action, options), + orientation: ActionsOrientation.HORIZONTAL, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + })); + + toolBar.setActions(prepareActions(menu.getPrimaryActions())); + this.headerFooterCompositeBarDispoables.add(menu.onDidChange(() => toolBar.setActions(prepareActions(menu.getPrimaryActions())))); + + headerArea.appendChild(globalHeaderContainer); + return headerArea; + } + + protected override getToolbarWidth(): number { + if (this.getCompositeBarPosition() === CompositeBarPosition.TOP) { + return 22; + } + return super.getToolbarWidth(); + } + + private headerActionViewItemProvider(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { + if (action.id === ToggleAuxiliaryBarAction.ID) { + return this.instantiationService.createInstance(ActionViewItem, undefined, action, options); + } + + return undefined; } override toJSON(): object { diff --git a/code/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/code/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index 3d382b3e39d..21f22875e1f 100644 --- a/code/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/code/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -14,30 +14,70 @@ background-color: var(--vscode-sideBar-background); } +.monaco-workbench .part.auxiliarybar .title-actions .actions-container { + justify-content: flex-end; +} + +.monaco-workbench .part.auxiliarybar .title-actions .action-item { + margin-right: 4px; +} + +.monaco-workbench .part.auxiliarybar > .title > .title-label { + flex: 1; +} + +.monaco-workbench .part.auxiliarybar > .title > .title-label h2 { + text-transform: uppercase; +} + .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container { flex: 1; } +.monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus { + outline: 0 !important; /* activity bar indicates focus custom */ +} + +.monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { + border-radius: 0px; + outline-offset: 2px; +} + +.monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { + position: absolute; + left: 6px; /* place icon in center */ +} + +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { - border-top-color: var(--vscode-panelTitle-activeBorder) !important; + border-top-color: var(--vscode-activityBarTop-activeBorder) !important; } +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label { color: var(--vscode-sideBarTitle-foreground) !important; } +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) solid 1px !important; } +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label, .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) dashed 1px !important; } -.monaco-workbench .auxiliarybar.part.pane-composite-part > .composite.title.has-composite-bar > .title-actions { +.monaco-workbench .auxiliarybar.part.pane-composite-part > .composite.title > .title-actions { flex: inherit; } diff --git a/code/src/vs/workbench/browser/parts/banner/bannerPart.ts b/code/src/vs/workbench/browser/parts/banner/bannerPart.ts index 3725e594c97..91c8e9902bb 100644 --- a/code/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/code/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -86,7 +86,7 @@ export class BannerPart extends Part implements IBannerService { })); // Track focus - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); BannerFocused.bindTo(scopedContextKeyService).set(true); return this.element; diff --git a/code/src/vs/workbench/browser/parts/compositeBar.ts b/code/src/vs/workbench/browser/parts/compositeBar.ts index 27d0b3738ea..a9fc39c84ed 100644 --- a/code/src/vs/workbench/browser/parts/compositeBar.ts +++ b/code/src/vs/workbench/browser/parts/compositeBar.ts @@ -37,6 +37,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { constructor( private viewDescriptorService: IViewDescriptorService, private targetContainerLocation: ViewContainerLocation, + private orientation: ActionsOrientation, private openComposite: (id: string, focus?: boolean) => Promise, private moveComposite: (from: string, to: string, before?: Before2D) => void, private getItems: () => ICompositeBarItem[] @@ -93,7 +94,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } const items = this.getItems(); - const before = this.targetContainerLocation === ViewContainerLocation.Panel ? before2d?.horizontallyBefore : before2d?.verticallyBefore; + const before = this.orientation === ActionsOrientation.HORIZONTAL ? before2d?.horizontallyBefore : before2d?.verticallyBefore; return items.filter(item => item.visible).findIndex(item => item.id === targetId) + (before ? 0 : 1); } diff --git a/code/src/vs/workbench/browser/parts/compositeBarActions.ts b/code/src/vs/workbench/browser/parts/compositeBarActions.ts index cfa2d348aa0..6ae11c51361 100644 --- a/code/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/code/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -26,7 +26,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { URI } from 'vs/base/common/uri'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; export interface ICompositeBar { diff --git a/code/src/vs/workbench/browser/parts/compositePart.ts b/code/src/vs/workbench/browser/parts/compositePart.ts index 81a034ffa91..d1617f55cb5 100644 --- a/code/src/vs/workbench/browser/parts/compositePart.ts +++ b/code/src/vs/workbench/browser/parts/compositePart.ts @@ -33,9 +33,9 @@ import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; export interface ICompositeTitleLabel { @@ -70,7 +70,7 @@ export abstract class CompositePart extends Part { private activeComposite: Composite | undefined; private lastActiveCompositeId: string; private readonly instantiatedCompositeItems = new Map(); - private titleLabel: ICompositeTitleLabel | undefined; + protected titleLabel: ICompositeTitleLabel | undefined; private progressBar: ProgressBar | undefined; private contentAreaSize: Dimension | undefined; private readonly actionsListener = this._register(new MutableDisposable()); @@ -97,7 +97,7 @@ export abstract class CompositePart extends Part { super(id, options, themeService, storageService, layoutService); this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId); - this.toolbarHoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + this.toolbarHoverDelegate = this._register(createInstantHoverDelegate()); } protected openComposite(id: string, focus?: boolean): Composite | undefined { @@ -438,6 +438,14 @@ export abstract class CompositePart extends Part { }; } + protected createHeaderArea(): HTMLElement { + return $('.composite'); + } + + protected createFooterArea(): HTMLElement { + return $('.composite'); + } + override updateStyles(): void { super.updateStyles(); diff --git a/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index 7e4dd2aeddf..3ea4811e4c4 100644 --- a/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -5,6 +5,7 @@ import { onDidChangeFullscreen } from 'vs/base/browser/browser'; import { hide, show } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isNative } from 'vs/base/common/platform'; @@ -171,6 +172,22 @@ export class AuxiliaryEditorPart { disposables.dispose(); })); disposables.add(Event.once(this.lifecycleService.onDidShutdown)(() => disposables.dispose())); + disposables.add(auxiliaryWindow.onBeforeUnload(event => { + for (const group of editorPart.groups) { + for (const editor of group.editors) { + const canMove = editor.canMove(mainWindow.vscodeWindowId); + if (typeof canMove === 'string') { + // Closing an auxiliary window with opened editors + // will move the editors to the main window. As such, + // we need to validate that we can move and otherwise + // prevent the window from closing. + group.openEditor(editor); + event.veto(canMove); + break; + } + } + } + })); // Layout: specifically `onWillLayout` to have a chance // to build the aux editor part before other components diff --git a/code/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/code/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index bcd00491191..f4314ae0dbb 100644 --- a/code/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -13,7 +13,7 @@ import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/bina import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; /** @@ -24,6 +24,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { static override readonly ID = BINARY_DIFF_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -33,7 +34,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); + super(group, telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); } getMetadata(): string | undefined { diff --git a/code/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/code/src/vs/workbench/browser/parts/editor/binaryEditor.ts index cba829c7be5..52a01908818 100644 --- a/code/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -13,6 +13,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ByteSize } from 'vs/platform/files/common/files'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise; @@ -33,12 +34,13 @@ export abstract class BaseBinaryResourceEditor extends EditorPlaceholder { constructor( id: string, + group: IEditorGroup, private readonly callbacks: IOpenCallbacks, telemetryService: ITelemetryService, themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } override getTitle(): string { diff --git a/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 2a9d829b3ff..e2fefb174e6 100644 --- a/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -40,8 +40,8 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; import { defaultBreadcrumbsWidgetStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Emitter } from 'vs/base/common/event'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { nativeHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; class OutlineItem extends BreadcrumbsItem { @@ -229,7 +229,7 @@ export class BreadcrumbsControl { this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); - this._hoverDelegate = nativeHoverDelegate; + this._hoverDelegate = getDefaultHoverDelegate('mouse'); this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget)); this.hide(); @@ -517,7 +517,7 @@ export class BreadcrumbsControl { this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick); } } else { - element.outline.reveal(element, { pinned }, group === SIDE_GROUP); + element.outline.reveal(element, { pinned }, group === SIDE_GROUP, false); } } @@ -611,14 +611,15 @@ registerAction2(class ToggleBreadcrumb extends Action2 { category: Categories.View, toggled: { condition: ContextKeyExpr.equals('config.breadcrumbs.enabled', true), - title: localize('cmd.toggle2', "Breadcrumbs"), - mnemonicTitle: localize({ key: 'miBreadcrumbs2', comment: ['&& denotes a mnemonic'] }, "&&Breadcrumbs") + title: localize('cmd.toggle2', "Toggle Breadcrumbs"), + mnemonicTitle: localize({ key: 'miBreadcrumbs2', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breadcrumbs") }, menu: [ { id: MenuId.CommandPalette }, { id: MenuId.MenubarAppearanceMenu, group: '4_editor', order: 2 }, { id: MenuId.NotebookToolbar, group: 'notebookLayout', order: 2 }, - { id: MenuId.StickyScrollContext } + { id: MenuId.StickyScrollContext }, + { id: MenuId.NotebookStickyScrollContext, group: 'notebookView', order: 2 } ] }); } @@ -859,7 +860,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ return (>input).reveal(element, { pinned: true, preserveFocus: false - }, true); + }, true, false); } } }); diff --git a/code/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/code/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 26f77202d53..13c2df31119 100644 --- a/code/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/code/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -31,8 +31,6 @@ import { IOutline, IOutlineComparator } from 'vs/workbench/services/outline/brow import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { nativeHoverDelegate } from 'vs/platform/hover/browser/hover'; interface ILayoutInfo { maxHeight: number; @@ -215,13 +213,12 @@ class FileRenderer implements ITreeRenderer, index: number, templateData: IResourceLabel): void { @@ -377,7 +374,7 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { 'BreadcrumbsFilePicker', container, new FileVirtualDelegate(), - [this._instantiationService.createInstance(FileRenderer, labels, nativeHoverDelegate)], + [this._instantiationService.createInstance(FileRenderer, labels)], this._instantiationService.createInstance(FileDataSource), { multipleSelectionSupport: false, @@ -510,7 +507,7 @@ export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { protected async _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise { this._onWillPickElement.fire(); const outline: IOutline = this._tree.getInput(); - await outline.reveal(element, options, sideBySide); + await outline.reveal(element, options, sideBySide, false); return true; } } diff --git a/code/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/code/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts new file mode 100644 index 00000000000..7a782b9ee3e --- /dev/null +++ b/code/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { localize2, localize } from 'vs/nls'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; +import { TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide'; +export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange'; +export const GOTO_PREVIOUS_CHANGE = 'workbench.action.compareEditor.previousChange'; +export const DIFF_FOCUS_PRIMARY_SIDE = 'workbench.action.compareEditor.focusPrimarySide'; +export const DIFF_FOCUS_SECONDARY_SIDE = 'workbench.action.compareEditor.focusSecondarySide'; +export const DIFF_FOCUS_OTHER_SIDE = 'workbench.action.compareEditor.focusOtherSide'; +export const DIFF_OPEN_SIDE = 'workbench.action.compareEditor.openSide'; +export const TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE = 'toggle.diff.ignoreTrimWhitespace'; +export const DIFF_SWAP_SIDES = 'workbench.action.compareEditor.swapSides'; + +export function registerDiffEditorCommands(): void { + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: GOTO_NEXT_CHANGE, + weight: KeybindingWeight.WorkbenchContrib, + when: TextCompareEditorVisibleContext, + primary: KeyMod.Alt | KeyCode.F5, + handler: accessor => navigateInDiffEditor(accessor, true) + }); + + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: GOTO_NEXT_CHANGE, + title: localize2('compare.nextChange', 'Go to Next Change'), + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: GOTO_PREVIOUS_CHANGE, + weight: KeybindingWeight.WorkbenchContrib, + when: TextCompareEditorVisibleContext, + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F5, + handler: accessor => navigateInDiffEditor(accessor, false) + }); + + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: GOTO_PREVIOUS_CHANGE, + title: localize2('compare.previousChange', 'Go to Previous Change'), + } + }); + + function getActiveTextDiffEditor(accessor: ServicesAccessor): TextDiffEditor | undefined { + const editorService = accessor.get(IEditorService); + + for (const editor of [editorService.activeEditorPane, ...editorService.visibleEditorPanes]) { + if (editor instanceof TextDiffEditor) { + return editor; + } + } + + return undefined; + } + + function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + + if (activeTextDiffEditor) { + activeTextDiffEditor.getControl()?.goToDiff(next ? 'next' : 'previous'); + } + } + + enum FocusTextDiffEditorMode { + Original, + Modified, + Toggle + } + + function focusInDiffEditor(accessor: ServicesAccessor, mode: FocusTextDiffEditorMode): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + + if (activeTextDiffEditor) { + switch (mode) { + case FocusTextDiffEditorMode.Original: + activeTextDiffEditor.getControl()?.getOriginalEditor().focus(); + break; + case FocusTextDiffEditorMode.Modified: + activeTextDiffEditor.getControl()?.getModifiedEditor().focus(); + break; + case FocusTextDiffEditorMode.Toggle: + if (activeTextDiffEditor.getControl()?.getModifiedEditor().hasWidgetFocus()) { + return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original); + } else { + return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified); + } + } + } + } + + function toggleDiffSideBySide(accessor: ServicesAccessor): void { + const configurationService = accessor.get(IConfigurationService); + + const newValue = !configurationService.getValue('diffEditor.renderSideBySide'); + configurationService.updateValue('diffEditor.renderSideBySide', newValue); + } + + function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor): void { + const configurationService = accessor.get(IConfigurationService); + + const newValue = !configurationService.getValue('diffEditor.ignoreTrimWhitespace'); + configurationService.updateValue('diffEditor.ignoreTrimWhitespace', newValue); + } + + async function swapDiffSides(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + + const diffEditor = getActiveTextDiffEditor(accessor); + const activeGroup = diffEditor?.group; + const diffInput = diffEditor?.input; + if (!diffEditor || typeof activeGroup === 'undefined' || !(diffInput instanceof DiffEditorInput) || !diffInput.modified.resource) { + return; + } + + const untypedDiffInput = diffInput.toUntyped({ preserveViewState: activeGroup.id, preserveResource: true }); + if (!untypedDiffInput) { + return; + } + + // Since we are about to replace the diff editor, make + // sure to first open the modified side if it is not + // yet opened. This ensures that the swapping is not + // bringing up a confirmation dialog to save. + if (diffInput.modified.isModified() && editorService.findEditors({ resource: diffInput.modified.resource, typeId: diffInput.modified.typeId, editorId: diffInput.modified.editorId }).length === 0) { + await editorService.openEditor({ + ...untypedDiffInput.modified, + options: { + ...untypedDiffInput.modified.options, + pinned: true, + inactive: true + } + }, activeGroup); + } + + // Replace the input with the swapped variant + await editorService.replaceEditors([ + { + editor: diffInput, + replacement: { + ...untypedDiffInput, + original: untypedDiffInput.modified, + modified: untypedDiffInput.original, + options: { + ...untypedDiffInput.options, + pinned: true + } + } + } + ], activeGroup); + } + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: TOGGLE_DIFF_SIDE_BY_SIDE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => toggleDiffSideBySide(accessor) + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DIFF_FOCUS_PRIMARY_SIDE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified) + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DIFF_FOCUS_SECONDARY_SIDE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original) + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DIFF_FOCUS_OTHER_SIDE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Toggle) + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => toggleDiffIgnoreTrimWhitespace(accessor) + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DIFF_SWAP_SIDES, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: accessor => swapDiffSides(accessor) + }); + + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: TOGGLE_DIFF_SIDE_BY_SIDE, + title: localize2('toggleInlineView', "Toggle Inline View"), + category: localize('compare', "Compare") + }, + when: TextCompareEditorActiveContext + }); + + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: DIFF_SWAP_SIDES, + title: localize2('swapDiffSides', "Swap Left and Right Editor Side"), + category: localize('compare', "Compare") + }, + when: ContextKeyExpr.and(TextCompareEditorActiveContext, ActiveCompareEditorCanSwapContext) + }); +} diff --git a/code/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/code/src/vs/workbench/browser/parts/editor/editor.contribution.ts index ca21ad13864..1611076ea51 100644 --- a/code/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/code/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -11,7 +11,7 @@ import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, EditorPartMultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, EditorTabsVisibleContext, ActiveEditorLastInGroupContext, EditorPartMaximizedEditorGroupContext, MultipleEditorGroupsContext, InEditorZenModeContext, - IsAuxiliaryEditorPartContext, ActiveCompareEditorOriginalWriteableContext + IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -47,12 +47,13 @@ import { } from 'vs/workbench/browser/parts/editor/editorActions'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, - CLOSE_PINNED_EDITOR_COMMAND_ID, CLOSE_SAVED_EDITORS_COMMAND_ID, GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, KEEP_EDITOR_COMMAND_ID, PIN_EDITOR_COMMAND_ID, SHOW_EDITORS_IN_GROUP, SPLIT_EDITOR_DOWN, SPLIT_EDITOR_LEFT, - SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, TOGGLE_DIFF_SIDE_BY_SIDE, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup as registerEditorCommands, REOPEN_WITH_COMMAND_ID, + CLOSE_PINNED_EDITOR_COMMAND_ID, CLOSE_SAVED_EDITORS_COMMAND_ID, KEEP_EDITOR_COMMAND_ID, PIN_EDITOR_COMMAND_ID, SHOW_EDITORS_IN_GROUP, SPLIT_EDITOR_DOWN, SPLIT_EDITOR_LEFT, + SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup as registerEditorCommands, REOPEN_WITH_COMMAND_ID, TOGGLE_LOCK_GROUP_COMMAND_ID, UNLOCK_GROUP_COMMAND_ID, SPLIT_EDITOR_IN_GROUP, JOIN_EDITOR_IN_GROUP, FOCUS_FIRST_SIDE_EDITOR, FOCUS_SECOND_SIDE_EDITOR, TOGGLE_SPLIT_EDITOR_IN_GROUP_LAYOUT, LOCK_GROUP_COMMAND_ID, SPLIT_EDITOR, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, - NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID, DIFF_SWAP_SIDES + NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, TOGGLE_DIFF_SIDE_BY_SIDE, DIFF_SWAP_SIDES } from './diffEditorCommands'; import { inQuickPickContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; @@ -570,11 +571,8 @@ appendEditorToolItem( CLOSE_ORDER - 1, // immediately to the left of close action ); -const previousChangeIcon = registerIcon('diff-editor-previous-change', Codicon.arrowUp, localize('previousChangeIcon', 'Icon for the previous change action in the diff editor.')); -const nextChangeIcon = registerIcon('diff-editor-next-change', Codicon.arrowDown, localize('nextChangeIcon', 'Icon for the next change action in the diff editor.')); -const toggleWhitespace = registerIcon('diff-editor-toggle-whitespace', Codicon.whitespace, localize('toggleWhitespace', 'Icon for the toggle whitespace action in the diff editor.')); - // Diff Editor Title Menu: Previous Change +const previousChangeIcon = registerIcon('diff-editor-previous-change', Codicon.arrowUp, localize('previousChangeIcon', 'Icon for the previous change action in the diff editor.')); appendEditorToolItem( { id: GOTO_PREVIOUS_CHANGE, @@ -588,6 +586,7 @@ appendEditorToolItem( ); // Diff Editor Title Menu: Next Change +const nextChangeIcon = registerIcon('diff-editor-next-change', Codicon.arrowDown, localize('nextChangeIcon', 'Icon for the next change action in the diff editor.')); appendEditorToolItem( { id: GOTO_NEXT_CHANGE, @@ -607,12 +606,13 @@ appendEditorToolItem( title: localize('swapDiffSides', "Swap Left and Right Side"), icon: Codicon.arrowSwap }, - ContextKeyExpr.and(TextCompareEditorActiveContext, ActiveCompareEditorOriginalWriteableContext), + ContextKeyExpr.and(TextCompareEditorActiveContext, ActiveCompareEditorCanSwapContext), 15, undefined, undefined ); +const toggleWhitespace = registerIcon('diff-editor-toggle-whitespace', Codicon.whitespace, localize('toggleWhitespace', 'Icon for the toggle whitespace action in the diff editor.')); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, diff --git a/code/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/code/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index ddcce4134c0..66940e29b77 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -80,7 +80,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution if (workingCopyResult?.condition === condition) { if ( workingCopyResult.workingCopy.isDirty() && - this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource, workingCopyResult.reason).mode !== AutoSaveMode.OFF ) { this.discardAutoSave(workingCopyResult.workingCopy); @@ -96,7 +96,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution editorResult?.condition === condition && !editorResult.editor.editor.isDisposed() && editorResult.editor.editor.isDirty() && - this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor, editorResult.reason).mode !== AutoSaveMode.OFF ) { this.waitingOnConditionAutoSaveEditors.delete(resource); @@ -151,7 +151,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution return; // no auto save for non-dirty, readonly or untitled editors } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { // Determine if we need to save all. In case of a window focus change we also save if // auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change) @@ -198,7 +198,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution continue; // we never auto save untitled working copies } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { @@ -257,12 +257,13 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Save if dirty and unless prevented by other conditions such as error markers if (workingCopy.isDirty()) { - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const reason = SaveReason.AUTO; + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(), workingCopy.typeId); - workingCopy.save({ reason: SaveReason.AUTO }); + workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { - this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason: SaveReason.AUTO, condition: autoSaveMode.reason }); + this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason, condition: autoSaveMode.reason }); } } }, autoSaveAfterDelay); diff --git a/code/src/vs/workbench/browser/parts/editor/editorCommands.ts b/code/src/vs/workbench/browser/parts/editor/editorCommands.ts index 68cd3a6b328..89795c71057 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -16,7 +16,7 @@ import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; -import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext, TextCompareEditorVisibleContext } from 'vs/workbench/common/contextkeys'; +import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -41,6 +41,7 @@ import { IEditorResolverService } from 'vs/workbench/services/editor/common/edit import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { DIFF_FOCUS_OTHER_SIDE, DIFF_FOCUS_PRIMARY_SIDE, DIFF_FOCUS_SECONDARY_SIDE, DIFF_OPEN_SIDE, registerDiffEditorCommands } from './diffEditorCommands'; export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors'; export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup'; @@ -65,16 +66,6 @@ export const REOPEN_WITH_COMMAND_ID = 'workbench.action.reopenWithEditor'; export const PIN_EDITOR_COMMAND_ID = 'workbench.action.pinEditor'; export const UNPIN_EDITOR_COMMAND_ID = 'workbench.action.unpinEditor'; -export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide'; -export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange'; -export const GOTO_PREVIOUS_CHANGE = 'workbench.action.compareEditor.previousChange'; -export const DIFF_FOCUS_PRIMARY_SIDE = 'workbench.action.compareEditor.focusPrimarySide'; -export const DIFF_FOCUS_SECONDARY_SIDE = 'workbench.action.compareEditor.focusSecondarySide'; -export const DIFF_FOCUS_OTHER_SIDE = 'workbench.action.compareEditor.focusOtherSide'; -export const DIFF_OPEN_SIDE = 'workbench.action.compareEditor.openSide'; -export const TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE = 'toggle.diff.ignoreTrimWhitespace'; -export const DIFF_SWAP_SIDES = 'workbench.action.compareEditor.swapSides'; - export const SPLIT_EDITOR = 'workbench.action.splitEditor'; export const SPLIT_EDITOR_UP = 'workbench.action.splitEditorUp'; export const SPLIT_EDITOR_DOWN = 'workbench.action.splitEditorDown'; @@ -373,212 +364,6 @@ function registerEditorGroupsLayoutCommands(): void { }); } -function registerDiffEditorCommands(): void { - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: GOTO_NEXT_CHANGE, - weight: KeybindingWeight.WorkbenchContrib, - when: TextCompareEditorVisibleContext, - primary: KeyMod.Alt | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, true) - }); - - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: GOTO_NEXT_CHANGE, - title: localize2('compare.nextChange', 'Go to Next Change'), - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: GOTO_PREVIOUS_CHANGE, - weight: KeybindingWeight.WorkbenchContrib, - when: TextCompareEditorVisibleContext, - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, false) - }); - - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: GOTO_PREVIOUS_CHANGE, - title: localize2('compare.previousChange', 'Go to Previous Change'), - } - }); - - function getActiveTextDiffEditor(accessor: ServicesAccessor): TextDiffEditor | undefined { - const editorService = accessor.get(IEditorService); - - for (const editor of [editorService.activeEditorPane, ...editorService.visibleEditorPanes]) { - if (editor instanceof TextDiffEditor) { - return editor; - } - } - - return undefined; - } - - function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); - - if (activeTextDiffEditor) { - activeTextDiffEditor.getControl()?.goToDiff(next ? 'next' : 'previous'); - } - } - - enum FocusTextDiffEditorMode { - Original, - Modified, - Toggle - } - - function focusInDiffEditor(accessor: ServicesAccessor, mode: FocusTextDiffEditorMode): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); - - if (activeTextDiffEditor) { - switch (mode) { - case FocusTextDiffEditorMode.Original: - activeTextDiffEditor.getControl()?.getOriginalEditor().focus(); - break; - case FocusTextDiffEditorMode.Modified: - activeTextDiffEditor.getControl()?.getModifiedEditor().focus(); - break; - case FocusTextDiffEditorMode.Toggle: - if (activeTextDiffEditor.getControl()?.getModifiedEditor().hasWidgetFocus()) { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original); - } else { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified); - } - } - } - } - - function toggleDiffSideBySide(accessor: ServicesAccessor): void { - const configurationService = accessor.get(IConfigurationService); - - const newValue = !configurationService.getValue('diffEditor.renderSideBySide'); - configurationService.updateValue('diffEditor.renderSideBySide', newValue); - } - - function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor): void { - const configurationService = accessor.get(IConfigurationService); - - const newValue = !configurationService.getValue('diffEditor.ignoreTrimWhitespace'); - configurationService.updateValue('diffEditor.ignoreTrimWhitespace', newValue); - } - - async function swapDiffSides(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - - const diffEditor = getActiveTextDiffEditor(accessor); - const activeGroup = diffEditor?.group; - const diffInput = diffEditor?.input; - if (!diffEditor || typeof activeGroup === 'undefined' || !(diffInput instanceof DiffEditorInput) || !diffInput.modified.resource) { - return; - } - - const untypedDiffInput = diffInput.toUntyped({ preserveViewState: activeGroup.id, preserveResource: true }); - if (!untypedDiffInput) { - return; - } - - // Since we are about to replace the diff editor, make - // sure to first open the modified side if it is not - // yet opened. This ensures that the swapping is not - // bringing up a confirmation dialog to save. - if (diffInput.modified.isModified() && editorService.findEditors({ resource: diffInput.modified.resource, typeId: diffInput.modified.typeId, editorId: diffInput.modified.editorId }).length === 0) { - await editorService.openEditor({ - ...untypedDiffInput.modified, - options: { - ...untypedDiffInput.modified.options, - pinned: true, - inactive: true - } - }, activeGroup); - } - - // Replace the input with the swapped variant - await editorService.replaceEditors([ - { - editor: diffInput, - replacement: { - ...untypedDiffInput, - original: untypedDiffInput.modified, - modified: untypedDiffInput.original, - options: { - ...untypedDiffInput.options, - pinned: true - } - } - } - ], activeGroup); - } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: TOGGLE_DIFF_SIDE_BY_SIDE, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: undefined, - handler: accessor => toggleDiffSideBySide(accessor) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: DIFF_FOCUS_PRIMARY_SIDE, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: DIFF_FOCUS_SECONDARY_SIDE, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: DIFF_FOCUS_OTHER_SIDE, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Toggle) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: undefined, - handler: accessor => toggleDiffIgnoreTrimWhitespace(accessor) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: DIFF_SWAP_SIDES, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: undefined, - handler: accessor => swapDiffSides(accessor) - }); - - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: TOGGLE_DIFF_SIDE_BY_SIDE, - title: localize2('toggleInlineView', "Toggle Inline View"), - category: localize('compare', "Compare") - }, - when: TextCompareEditorActiveContext - }); - - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: DIFF_SWAP_SIDES, - title: localize2('swapDiffSides', "Swap Left and Right Editor Side"), - category: localize('compare', "Compare") - }, - when: TextCompareEditorActiveContext - }); -} - function registerOpenEditorAPICommands(): void { function mixinContext(context: IOpenEvent | undefined, options: ITextEditorOptions | undefined, column: EditorGroupColumn | undefined): [ITextEditorOptions | undefined, EditorGroupColumn | undefined] { diff --git a/code/src/vs/workbench/browser/parts/editor/editorConfiguration.ts b/code/src/vs/workbench/browser/parts/editor/editorConfiguration.ts index 9f3008f7385..0df198dd6de 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorConfiguration.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorConfiguration.ts @@ -93,7 +93,7 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc private registerListeners(): void { // Registered editors (debounced to reduce perf overhead) - Event.debounce(this.editorResolverService.onDidChangeEditorRegistrations, (_, e) => e)(() => this.updateDynamicEditorConfigurations()); + this._register(Event.debounce(this.editorResolverService.onDidChangeEditorRegistrations, (_, e) => e)(() => this.updateDynamicEditorConfigurations())); } private updateDynamicEditorConfigurations(): void { diff --git a/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts index f0bdfdd5bc9..bc8cadd1c0a 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, focusWindow, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, getWindow, getActiveElement } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -42,7 +42,7 @@ import { getMimeTypes } from 'vs/editor/common/services/languagesAssociations'; import { extname, isEqual } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -156,7 +156,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @IHostService private readonly hostService: IHostService + @IHostService private readonly hostService: IHostService, + @IDialogService private readonly dialogService: IDialogService ) { super(themeService); @@ -544,6 +545,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Visibility this._register(this.groupsView.onDidVisibilityChange(e => this.onDidVisibilityChange(e))); + + // Focus + this._register(this.onDidFocus(() => this.onDidGainFocus())); } private onDidGroupModelChange(e: IGroupModelChangeEvent): void { @@ -578,6 +582,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { case GroupModelChangeKind.EDITOR_DIRTY: this.onDidChangeEditorDirty(e.editor); break; + case GroupModelChangeKind.EDITOR_TRANSIENT: + this.onDidChangeEditorTransient(e.editor); + break; case GroupModelChangeKind.EDITOR_LABEL: this.onDidChangeEditorLabel(e.editor); break; @@ -714,7 +721,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close active one last if (activeEditor) { - this.doCloseEditor(activeEditor); + this.doCloseEditor(activeEditor, true); } } @@ -762,6 +769,17 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleControl.updateEditorDirty(editor); } + private onDidChangeEditorTransient(editor: EditorInput): void { + const transient = this.model.isTransient(editor); + + // Transient state overrides the `enablePreview` setting, + // so when an editor leaves the transient state, we have + // to ensure its preview state is also cleared. + if (!transient && !this.groupsView.partOptions.enablePreview) { + this.pinEditor(editor); + } + } + private onDidChangeEditorLabel(editor: EditorInput): void { // Forward to title control @@ -774,6 +792,18 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.editorPane.setVisible(visible); } + private onDidGainFocus(): void { + if (this.activeEditor) { + + // We aggressively clear the transient state of editors + // as soon as the group gains focus. This is to ensure + // that the transient state is not staying around when + // the user interacts with the editor. + + this.model.setTransient(this.activeEditor, false); + } + } + //#endregion //#region IEditorGroupView @@ -886,6 +916,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isSticky(editorOrIndex); } + isTransient(editorOrIndex: EditorInput | number): boolean { + return this.model.isTransient(editorOrIndex); + } + isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } @@ -943,9 +977,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { focus(): void { - // Ensure window focus - focusWindow(this.element); - // Pass focus to editor panes if (this.activeEditorPane) { this.activeEditorPane.focus(); @@ -1033,7 +1064,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Determine options const pinned = options?.sticky - || !this.groupsView.partOptions.enablePreview + || (!this.groupsView.partOptions.enablePreview && !options?.transient) || editor.isDirty() || (options?.pinned ?? typeof options?.index === 'number' /* unless specified, prefer to pin when opening with index */) || (typeof options?.index === 'number' && this.model.isSticky(options.index)) @@ -1042,6 +1073,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { index: options ? options.index : undefined, pinned, sticky: options?.sticky || (typeof options?.index === 'number' && this.model.isSticky(options.index)), + transient: !!options?.transient, active: this.count === 0 || !options || !options.inactive, supportSideBySide: internalOptions?.supportSideBySide }; @@ -1292,6 +1324,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private doMoveOrCopyEditorAcrossGroups(editor: EditorInput, target: EditorGroupView, openOptions?: IEditorOpenOptions, internalOptions?: IInternalMoveCopyOptions): void { const keepCopy = internalOptions?.keepCopy; + // Validate that we can move + if (!keepCopy) { + const canMove = editor.canMove(target.windowId); + if (typeof canMove === 'string') { + this.dialogService.error(canMove); + return; + } + } + // When moving/copying an editor, try to preserve as much view state as possible // by checking for the editor to be a text editor and creating the options accordingly // if so diff --git a/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 3daa20701a7..77d8a445857 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -62,6 +62,7 @@ export class EditorGroupWatermark extends Disposable { private readonly transientDisposables = this._register(new DisposableStore()); private enabled: boolean = false; private workbenchState: WorkbenchState; + private keybindingLabel?: KeybindingLabel; constructor( container: HTMLElement, @@ -145,8 +146,9 @@ export class EditorGroupWatermark extends Disposable { const dt = append(dl, $('dt')); dt.textContent = entry.text; const dd = append(dl, $('dd')); - const keybinding = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); - keybinding.set(keys); + this.keybindingLabel?.dispose(); + this.keybindingLabel = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); + this.keybindingLabel.set(keys); } }; @@ -162,5 +164,6 @@ export class EditorGroupWatermark extends Disposable { override dispose(): void { super.dispose(); this.clear(); + this.keybindingLabel?.dispose(); } } diff --git a/code/src/vs/workbench/browser/parts/editor/editorPane.ts b/code/src/vs/workbench/browser/parts/editor/editorPane.ts index 0f26fa1aa20..7a73fe46fc5 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -24,6 +24,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; +import { getWindowById } from 'vs/base/browser/dom'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -70,8 +71,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { protected _options: IEditorOptions | undefined; get options(): IEditorOptions | undefined { return this._options; } - private _group: IEditorGroup | undefined; - get group(): IEditorGroup | undefined { return this._group; } + get window() { return getWindowById(this.group.windowId, true).window; } /** * Should be overridden by editors that have their own ScopedContextKeyService @@ -80,6 +80,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { constructor( id: string, + readonly group: IEditorGroup, telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService @@ -145,22 +146,20 @@ export abstract class EditorPane extends Composite implements IEditorPane { this._options = options; } - override setVisible(visible: boolean, group?: IEditorGroup): void { + override setVisible(visible: boolean): void { super.setVisible(visible); // Propagate to Editor - this.setEditorVisible(visible, group); + this.setEditorVisible(visible); } /** - * Indicates that the editor control got visible or hidden in a specific group. A - * editor instance will only ever be visible in one editor group. + * Indicates that the editor control got visible or hidden. * * @param visible the state of visibility of this editor - * @param group the editor group this editor is in. */ - protected setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - this._group = group; + protected setEditorVisible(visible: boolean): void { + // Subclasses can implement } setBoundarySashes(_sashes: IBoundarySashes) { diff --git a/code/src/vs/workbench/browser/parts/editor/editorPanes.ts b/code/src/vs/workbench/browser/parts/editor/editorPanes.ts index 1094f315842..5d638871273 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IAction, toAction } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, createEditorOpenError, isEditorOpenError } from 'vs/workbench/common/editor'; +import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getActiveElement, getWindowById } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -28,7 +28,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IDialogService, IPromptButton, IPromptCancelButton } from 'vs/platform/dialogs/common/dialogs'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { mainWindow } from 'vs/base/browser/window'; export interface IOpenEditorResult { @@ -129,22 +128,7 @@ export class EditorPanes extends Disposable { async openEditor(editor: EditorInput, options: IEditorOptions | undefined, internalOptions: IInternalEditorOpenOptions | undefined, context: IEditorOpenContext = Object.create(null)): Promise { try { - - // Assert the `EditorInputCapabilities.AuxWindowUnsupported` condition - if (getWindow(this.editorGroupParent) !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) { - return await this.doShowError(createEditorOpenError(localize('editorUnsupportedInAuxWindow', "This type of editor cannot be opened in other windows yet."), [ - toAction({ - id: 'workbench.editor.action.closeEditor', label: localize('openFolder', "Close Editor"), run: async () => { - return this.groupView.closeEditor(editor); - } - }) - ], { forceMessage: true, forceSeverity: Severity.Warning }), editor, options, internalOptions, context); - } - - // Open editor normally - else { - return await this.doOpenEditor(this.getEditorPaneDescriptor(editor), editor, options, internalOptions, context); - } + return await this.doOpenEditor(this.getEditorPaneDescriptor(editor), editor, options, internalOptions, context); } catch (error) { // First check if caller instructed us to ignore error handling @@ -277,7 +261,7 @@ export class EditorPanes extends Disposable { if (focus && this.shouldRestoreFocus(activeElement)) { pane.focus(); } else if (!internalOptions?.preserveWindowOrder) { - this.hostService.moveTop(getWindow(this.editorGroupParent)); + this.hostService.moveTop(getWindowById(this.groupView.windowId, true).window); } } @@ -353,7 +337,7 @@ export class EditorPanes extends Disposable { show(container); // Indicate to editor that it is now visible - editorPane.setVisible(true, this.groupView); + editorPane.setVisible(true); // Layout if (this.pagePosition) { @@ -378,6 +362,11 @@ export class EditorPanes extends Disposable { const editorPaneContainer = document.createElement('div'); editorPaneContainer.classList.add('editor-instance'); + // It is cruicial to append the container to its parent before + // passing on to the create() method of the pane so that the + // right `window` can be determined in floating window cases. + this.editorPanesParent.appendChild(editorPaneContainer); + editorPane.create(editorPaneContainer); } @@ -393,7 +382,7 @@ export class EditorPanes extends Disposable { } // Otherwise instantiate new - const editorPane = this._register(descriptor.instantiate(this.instantiationService)); + const editorPane = this._register(descriptor.instantiate(this.instantiationService, this.groupView)); this.editorPanes.push(editorPane); return editorPane; @@ -472,7 +461,7 @@ export class EditorPanes extends Disposable { // the DOM to give a chance to persist certain state that // might depend on still being the active DOM element. this.safeRun(() => this._activeEditorPane?.clearInput()); - this.safeRun(() => this._activeEditorPane?.setVisible(false, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(false)); // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); @@ -492,7 +481,7 @@ export class EditorPanes extends Disposable { } setVisible(visible: boolean): void { - this.safeRun(() => this._activeEditorPane?.setVisible(visible, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(visible)); } layout(pagePosition: IDomNodePagePosition): void { diff --git a/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index 417314bc1f7..7a7691b3850 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -29,6 +29,7 @@ import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IEditorPlaceholderContents { icon: string; @@ -55,11 +56,12 @@ export abstract class EditorPlaceholder extends EditorPane { constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { @@ -100,7 +102,7 @@ export abstract class EditorPlaceholder extends EditorPane { // Icon const iconContainer = container.appendChild($('.editor-placeholder-icon-container')); - const iconWidget = new SimpleIconLabel(iconContainer); + const iconWidget = disposables.add(new SimpleIconLabel(iconContainer)); iconWidget.text = icon; // Label @@ -186,13 +188,14 @@ export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IStorageService storageService: IStorageService ) { - super(WorkspaceTrustRequiredPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(WorkspaceTrustRequiredPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } override getTitle(): string { @@ -223,18 +226,18 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService ) { - super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(ErrorPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise { const resource = input.resource; - const group = this.group; const error = options.error; const isFileNotFound = (error)?.fileOperationResult === FileOperationResult.FILE_NOT_FOUND; @@ -274,20 +277,20 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { } }; }); - } else if (group) { + } else { actions = [ { label: localize('retry', "Try Again"), - run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) + run: () => this.group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) } ]; } // Auto-reload when file is added - if (group && isFileNotFound && resource && this.fileService.hasProvider(resource)) { + if (isFileNotFound && resource && this.fileService.hasProvider(resource)) { disposables.add(this.fileService.onDidFilesChange(e => { if (e.contains(resource, FileChangeType.ADDED, FileChangeType.UPDATED)) { - group.openEditor(input, options); + this.group.openEditor(input, options); } })); } diff --git a/code/src/vs/workbench/browser/parts/editor/editorStatus.ts b/code/src/vs/workbench/browser/parts/editor/editorStatus.ts index e1ee2bc0d94..dc58c035fed 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -293,8 +293,6 @@ class TabFocusMode extends Disposable { const tabFocusModeConfig = configurationService.getValue('editor.tabFocusMode') === true ? true : false; TabFocus.setTabFocusMode(tabFocusModeConfig); - - this._onDidChange.fire(tabFocusModeConfig); } private registerListeners(): void { @@ -328,13 +326,15 @@ class EditorStatus extends Disposable { private readonly eolElement = this._register(new MutableDisposable()); private readonly languageElement = this._register(new MutableDisposable()); private readonly metadataElement = this._register(new MutableDisposable()); - private readonly currentProblemStatus = this._register(this.instantiationService.createInstance(ShowCurrentMarkerInStatusbarContribution)); + + private readonly currentMarkerStatus = this._register(this.instantiationService.createInstance(ShowCurrentMarkerInStatusbarContribution)); + private readonly tabFocusMode = this._register(this.instantiationService.createInstance(TabFocusMode)); + private readonly state = new State(); + private toRender: StateChange | undefined = undefined; + private readonly activeEditorListeners = this._register(new DisposableStore()); private readonly delayedRender = this._register(new MutableDisposable()); - private readonly tabFocusMode = this.instantiationService.createInstance(TabFocusMode); - - private toRender: StateChange | undefined = undefined; constructor( private readonly targetWindowId: number, @@ -366,7 +366,7 @@ class EditorStatus extends Disposable { } private registerCommands(): void { - CommandsRegistry.registerCommand({ id: `changeEditorIndentation${this.targetWindowId}`, handler: () => this.showIndentationPicker() }); + this._register(CommandsRegistry.registerCommand({ id: `changeEditorIndentation${this.targetWindowId}`, handler: () => this.showIndentationPicker() })); } private async showIndentationPicker(): Promise { @@ -634,7 +634,7 @@ class EditorStatus extends Disposable { this.onEncodingChange(activeEditorPane, activeCodeEditor); this.onIndentationChange(activeCodeEditor); this.onMetadataChange(activeEditorPane); - this.currentProblemStatus.update(activeCodeEditor); + this.currentMarkerStatus.update(activeCodeEditor); // Dispose old active editor listeners this.activeEditorListeners.clear(); @@ -662,7 +662,7 @@ class EditorStatus extends Disposable { // Hook Listener for Selection changes this.activeEditorListeners.add(Event.defer(activeCodeEditor.onDidChangeCursorPosition)(() => { this.onSelectionChange(activeCodeEditor); - this.currentProblemStatus.update(activeCodeEditor); + this.currentMarkerStatus.update(activeCodeEditor); })); // Hook Listener for language changes @@ -673,7 +673,7 @@ class EditorStatus extends Disposable { // Hook Listener for content changes this.activeEditorListeners.add(Event.accumulate(activeCodeEditor.onDidChangeModelContent)(e => { this.onEOLChange(activeCodeEditor); - this.currentProblemStatus.update(activeCodeEditor); + this.currentMarkerStatus.update(activeCodeEditor); const selections = activeCodeEditor.getSelections(); if (selections) { @@ -918,13 +918,16 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); + this.statusBarEntryAccessor = this._register(new MutableDisposable()); + this._register(markerService.onMarkerChanged(changedResources => this.onMarkerChanged(changedResources))); this._register(Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('problems.showCurrentInStatus'))(() => this.updateStatus())); } update(editor: ICodeEditor | undefined): void { this.editor = editor; + this.updateMarkers(); this.updateStatus(); } @@ -1022,26 +1025,26 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { resource: model.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); - this.markers.sort(compareMarker); + this.markers.sort(this.compareMarker); } else { this.markers = []; } this.updateStatus(); } -} -function compareMarker(a: IMarker, b: IMarker): number { - let res = compare(a.resource.toString(), b.resource.toString()); - if (res === 0) { - res = MarkerSeverity.compare(a.severity, b.severity); - } + private compareMarker(a: IMarker, b: IMarker): number { + let res = compare(a.resource.toString(), b.resource.toString()); + if (res === 0) { + res = MarkerSeverity.compare(a.severity, b.severity); + } - if (res === 0) { - res = Range.compareRangesUsingStarts(a, b); - } + if (res === 0) { + res = Range.compareRangesUsingStarts(a, b); + } - return res; + return res; + } } export class ShowLanguageExtensionsAction extends Action { diff --git a/code/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/code/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 218c8808205..d7a82ed5c16 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -44,8 +44,8 @@ import { IAuxiliaryEditorPart, MergeGroupMode } from 'vs/workbench/services/edit import { isMacintosh } from 'vs/base/common/platform'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; export class EditorCommandsContextActionRunner extends ActionRunner { diff --git a/code/src/vs/workbench/browser/parts/editor/editorWithViewState.ts b/code/src/vs/workbench/browser/parts/editor/editorWithViewState.ts index f01bedd8f59..e31756007e9 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorWithViewState.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorWithViewState.ts @@ -31,6 +31,7 @@ export abstract class AbstractEditorWithViewState extends Edit constructor( id: string, + group: IEditorGroup, viewStateStorageKey: string, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @@ -40,17 +41,17 @@ export abstract class AbstractEditorWithViewState extends Edit @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); this.viewState = this.getEditorMemento(editorGroupService, textResourceConfigurationService, viewStateStorageKey, 100); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { // Listen to close events to trigger `onWillCloseEditorInGroup` - this.groupListener.value = group?.onWillCloseEditor(e => this.onWillCloseEditor(e)); + this.groupListener.value = this.group.onWillCloseEditor(e => this.onWillCloseEditor(e)); - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } private onWillCloseEditor(e: IEditorCloseEvent): void { @@ -110,7 +111,7 @@ export abstract class AbstractEditorWithViewState extends Edit // - the user configured to not restore view state unless the editor is still opened in the group if ( (input.isDisposed() && !this.tracksDisposedEditorViewState()) || - (!this.shouldRestoreEditorViewState(input) && (!this.group || !this.group.contains(input))) + (!this.shouldRestoreEditorViewState(input) && !this.group.contains(input)) ) { this.clearEditorViewState(resource, this.group); } @@ -147,10 +148,6 @@ export abstract class AbstractEditorWithViewState extends Edit } private saveEditorViewState(resource: URI): void { - if (!this.group) { - return; - } - const editorViewState = this.computeEditorViewState(resource); if (!editorViewState) { return; @@ -160,7 +157,7 @@ export abstract class AbstractEditorWithViewState extends Edit } protected loadEditorViewState(input: EditorInput | undefined, context?: IEditorOpenContext): T | undefined { - if (!input || !this.group) { + if (!input) { return undefined; // we need valid input } diff --git a/code/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/code/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 1a6b5ca985e..24d85415eb8 100644 --- a/code/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/code/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -36,19 +36,23 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, stickyModel)); this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, unstickyModel)); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } - private handlePinnedTabsSeparateRowToolbars(): void { + private handlePinnedTabsLayoutChange(): void { if (this.groupView.count === 0) { // Do nothing as no tab bar is visible return; } + + const hadTwoTabBars = this.parent.classList.contains('two-tab-bars'); + const hasTwoTabBars = this.groupView.count !== this.groupView.stickyCount && this.groupView.stickyCount > 0; + // Ensure action toolbar is only visible once - if (this.groupView.count === this.groupView.stickyCount) { - this.parent.classList.toggle('two-tab-bars', false); - } else { - this.parent.classList.toggle('two-tab-bars', true); + this.parent.classList.toggle('two-tab-bars', hasTwoTabBars); + + if (hadTwoTabBars !== hasTwoTabBars) { + this.groupView.relayout(); } } @@ -85,7 +89,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleOpenedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } beforeCloseEditor(editor: EditorInput): void { @@ -111,7 +115,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleClosedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void { @@ -125,7 +129,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.openEditor(editor); } - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } else { if (this.model.isSticky(editor)) { @@ -144,14 +148,14 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.closeEditor(editor); this.stickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } unstickEditor(editor: EditorInput): void { this.stickyEditorTabsControl.closeEditor(editor); this.unstickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } setActive(isActive: boolean): void { diff --git a/code/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/code/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index becfd13cac2..c3d7a0cce9d 100644 --- a/code/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -122,6 +122,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState extends return this.editorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.editorControl?.onVisible(); diff --git a/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 21429e37334..3c298c26b1e 100644 --- a/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -58,6 +58,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -68,7 +69,7 @@ export class TextDiffEditor extends AbstractTextEditor imp @IFileService fileService: IFileService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); + super(TextDiffEditor.ID, group, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); } override getTitle(): string { @@ -171,7 +172,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "At least one file is not displayed in the text compare editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -222,7 +223,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Replace this editor with the binary one - (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + this.group.replaceEditors([{ editor: input, replacement: binaryDiffInput, options: { @@ -232,8 +233,8 @@ export class TextDiffEditor extends AbstractTextEditor imp // and do not control the initial intent that resulted // in us now opening as binary. activation: EditorActivation.PRESERVE, - pinned: this.group?.isPinned(input), - sticky: this.group?.isSticky(input) + pinned: this.group.isPinned(input), + sticky: this.group.isSticky(input) } }]); } @@ -365,8 +366,8 @@ export class TextDiffEditor extends AbstractTextEditor imp return this.diffEditorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.diffEditorControl?.onVisible(); diff --git a/code/src/vs/workbench/browser/parts/editor/textEditor.ts b/code/src/vs/workbench/browser/parts/editor/textEditor.ts index 563f12a1be7..afae0b6f31b 100644 --- a/code/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { isObject, assertIsDefined } from 'vs/base/common/types'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorOpenContext, IEditorPaneSelection, EditorPaneSelectionCompareResult, EditorPaneSelectionChangeReason, IEditorPaneWithSelection, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; +import { IEditorOpenContext, IEditorPaneSelection, EditorPaneSelectionCompareResult, EditorPaneSelectionChangeReason, IEditorPaneWithSelection, IEditorPaneSelectionChangeEvent, IEditorPaneScrollPosition, IEditorPaneWithScrolling } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { AbstractEditorWithViewState } from 'vs/workbench/browser/parts/editor/editorWithViewState'; @@ -22,7 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorOptions, ITextEditorOptions, TextEditorSelectionRevealType, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; @@ -46,13 +46,16 @@ export interface IEditorConfiguration { /** * The base class of editors that leverage any kind of text editor for the editing experience. */ -export abstract class AbstractTextEditor extends AbstractEditorWithViewState implements IEditorPaneWithSelection { +export abstract class AbstractTextEditor extends AbstractEditorWithViewState implements IEditorPaneWithSelection, IEditorPaneWithScrolling { private static readonly VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; protected readonly _onDidChangeSelection = this._register(new Emitter()); readonly onDidChangeSelection = this._onDidChangeSelection.event; + protected readonly _onDidChangeScroll = this._register(new Emitter()); + readonly onDidChangeScroll = this._onDidChangeScroll.event; + private editorContainer: HTMLElement | undefined; private hasPendingConfigurationChange: boolean | undefined; @@ -62,6 +65,7 @@ export abstract class AbstractTextEditor extends Abs constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -71,7 +75,7 @@ export abstract class AbstractTextEditor extends Abs @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService protected readonly fileService: IFileService ) { - super(id, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); + super(id, group, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); // Listen to configuration changes this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(e))); @@ -127,8 +131,8 @@ export abstract class AbstractTextEditor extends Abs return editorConfiguration; } - private computeAriaLabel(): string { - return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); + protected computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } private onDidChangeFileSystemProvider(scheme: string): void { @@ -185,6 +189,7 @@ export abstract class AbstractTextEditor extends Abs this._register(mainControl.onDidChangeModel(() => this.updateEditorConfiguration())); this._register(mainControl.onDidChangeCursorPosition(e => this._onDidChangeSelection.fire({ reason: this.toEditorPaneSelectionChangeReason(e) }))); this._register(mainControl.onDidChangeModelContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + this._register(mainControl.onDidScrollChange(() => this._onDidChangeScroll.fire())); } } @@ -255,12 +260,36 @@ export abstract class AbstractTextEditor extends Abs super.clearInput(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + getScrollPosition(): IEditorPaneScrollPosition { + const editor = this.getMainControl(); + if (!editor) { + throw new Error('Control has not yet been initialized'); + } + + return { + scrollTop: editor.getScrollTop(), + scrollLeft: editor.getScrollLeft(), + }; + } + + setScrollPosition(scrollPosition: IEditorPaneScrollPosition): void { + const editor = this.getMainControl(); + if (!editor) { + throw new Error('Control has not yet been initialized'); + } + + editor.setScrollTop(scrollPosition.scrollTop); + if (scrollPosition.scrollLeft) { + editor.setScrollLeft(scrollPosition.scrollLeft); + } + } + + protected override setEditorVisible(visible: boolean): void { if (visible) { this.consumePendingConfigurationChangeEvent(); } - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } protected override toEditorViewStateResource(input: EditorInput): URI | undefined { diff --git a/code/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/code/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 7a16fa667b8..0a3b885e01d 100644 --- a/code/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -18,7 +18,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ScrollType, ICodeEditorViewState } from 'vs/editor/common/editorCommon'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/model'; @@ -37,6 +37,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -46,7 +47,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< @IEditorService editorService: IEditorService, @IFileService fileService: IFileService ) { - super(id, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(id, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -130,6 +131,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { static readonly ID = 'workbench.editors.textResourceEditor'; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -141,7 +143,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { @ILanguageService private readonly languageService: ILanguageService, @IFileService fileService: IFileService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(TextResourceEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void { diff --git a/code/src/vs/workbench/browser/parts/globalCompositeBar.ts b/code/src/vs/workbench/browser/parts/globalCompositeBar.ts index 3490432e312..8301e27c643 100644 --- a/code/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/code/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -43,6 +43,7 @@ import { isString } from 'vs/base/common/types'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND } from 'vs/workbench/common/theme'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export class GlobalCompositeBar extends Disposable { @@ -309,6 +310,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction @ILogService private readonly logService: ILogService, @IActivityService activityService: IActivityService, @IInstantiationService instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService ) { const action = instantiationService.createInstance(CompositeBarAction, { id: ACCOUNTS_ACTIVITY_ID, @@ -391,7 +393,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction menus.push(noAccountsAvailableAction); break; } - const providerLabel = this.authenticationService.getLabel(providerId); + const providerLabel = this.authenticationService.getProvider(providerId).label; const accounts = this.groupedAccounts.get(providerId); if (!accounts) { if (this.problematicProviders.has(providerId)) { @@ -408,19 +410,22 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction } for (const account of accounts) { - const manageExtensionsAction = disposables.add(new Action(`configureSessions${account.label}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), undefined, true, () => { - return this.authenticationService.manageTrustedExtensionsForAccount(providerId, account.label); - })); + const manageExtensionsAction = toAction({ + id: `configureSessions${account.label}`, + label: localize('manageTrustedExtensions', "Manage Trusted Extensions"), + enabled: true, + run: () => this.commandService.executeCommand('_manageTrustedExtensionsForAccount', { providerId, accountLabel: account.label }) + }); - const providerSubMenuActions: Action[] = [manageExtensionsAction]; + const providerSubMenuActions: IAction[] = [manageExtensionsAction]; if (account.canSignOut) { - const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), undefined, true, async () => { - const allSessions = await this.authenticationService.getSessions(providerId); - const sessionsForAccount = allSessions.filter(s => s.account.label === account.label); - return await this.authenticationService.removeAccountSessions(providerId, account.label, sessionsForAccount); + providerSubMenuActions.push(toAction({ + id: 'signOut', + label: localize('signOut', "Sign Out"), + enabled: true, + run: () => this.commandService.executeCommand('_signOutOfAccount', { providerId, accountLabel: account.label }) })); - providerSubMenuActions.push(signOutAction); } const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${providerLabel})`, providerSubMenuActions); @@ -628,7 +633,8 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV @ISecretStorageService secretStorageService: ISecretStorageService, @ILogService logService: ILogService, @IActivityService activityService: IActivityService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @ICommandService commandService: ICommandService ) { super(() => [], { ...options, @@ -638,7 +644,7 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV }), hoverOptions, compact: true, - }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService); + }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService); } } diff --git a/code/src/vs/workbench/browser/parts/media/compositepart.css b/code/src/vs/workbench/browser/parts/media/compositepart.css index fde7cdb706c..98f07d9cf18 100644 --- a/code/src/vs/workbench/browser/parts/media/compositepart.css +++ b/code/src/vs/workbench/browser/parts/media/compositepart.css @@ -7,6 +7,7 @@ height: 100%; } +.monaco-workbench .part > .composite.header-or-footer, .monaco-workbench .part > .composite.title { display: flex; } @@ -14,4 +15,4 @@ .monaco-workbench .part > .composite.title > .title-actions { flex: 1; padding-left: 5px; -} \ No newline at end of file +} diff --git a/code/src/vs/workbench/browser/parts/media/paneCompositePart.css b/code/src/vs/workbench/browser/parts/media/paneCompositePart.css index 9250cd44de1..52baa5324f7 100644 --- a/code/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/code/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -20,11 +20,31 @@ display: none; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container { +.monaco-workbench .pane-composite-part > .header-or-footer { + padding-left: 4px; + padding-right: 4px; + background-color: var(--vscode-activityBarTop-background); +} + +.monaco-workbench .pane-composite-part > .header { + border-bottom: 1px solid var(--vscode-sideBarActivityBarTop-border); +} + +.monaco-workbench .pane-composite-part > .footer { + border-top: 1px solid var(--vscode-sideBarActivityBarTop-border); +} + +.monaco-workbench .pane-composite-part > .title > .composite-bar-container, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container { display: flex; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { +.monaco-workbench .pane-composite-part > .header-or-footer .composite-bar-container { + flex: 1; +} + +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-label.codicon-more { display: flex; align-items: center; justify-content: center; @@ -33,12 +53,14 @@ color: inherit !important; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ height: 35px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item { text-transform: uppercase; padding-left: 10px; padding-right: 10px; @@ -48,22 +70,27 @@ display: flex; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { - height: 24px; +.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { + height: 35px; /* matches height of composite container */ padding: 0 5px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { font-size: 18px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon), +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { width: 16px; height: 16px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { content: ''; width: 2px; height: 24px; @@ -77,26 +104,33 @@ } .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over > .composite-bar > .monaco-action-bar .action-item::after { display: block; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::before { left: 1px; margin-left: -2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item::after { right: 1px; margin-right: -2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { + +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { left: 2px; margin-left: -2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { right: 2px; margin-right: -2px; } @@ -104,7 +138,11 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right::after { transition-delay: 0s; } @@ -112,39 +150,52 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, .monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.right + .action-item::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.left::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:last-of-type.right::after, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over-head > .composite-bar > .monaco-action-bar .action-item:first-of-type::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container.dragged-over-tail > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { opacity: 1; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { margin-right: 0; padding: 2px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { border-radius: 0; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.icon) .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label.codicon { background: none !important; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label { margin-right: 0; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge { margin-left: 8px; display: flex; align-items: center; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge { margin-left: 0px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .badge .badge-content { padding: 3px 5px; border-radius: 11px; font-size: 11px; @@ -158,7 +209,8 @@ position: relative; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { position: absolute; top: 0; bottom: 0; @@ -170,9 +222,10 @@ z-index: 2; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact .badge-content { position: absolute; - top: 11px; + top: 17px; right: 0px; font-size: 9px; font-weight: 600; @@ -184,7 +237,8 @@ text-align: center; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { mask-size: 11px; -webkit-mask-size: 11px; top: 3px; @@ -192,7 +246,8 @@ } /* active item indicator */ -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { position: absolute; z-index: 1; bottom: 0; @@ -201,44 +256,59 @@ height: 100%; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .active-item-indicator { top: -4px; left: 10px; width: calc(100% - 20px); } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .active-item-indicator { top: 1px; left: 2px; width: calc(100% - 4px); } +.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon.checked, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon.checked { + background-color: var(--vscode-activityBarTop-activeBackground); +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { content: ""; position: absolute; z-index: 1; - bottom: 0; + bottom: 2px; width: 100%; height: 0; border-top-width: 1px; border-top-style: solid; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.clicked:not(.checked):focus .active-item-indicator:before { border-top-color: transparent !important; /* hides border on clicked state */ } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .active-item-indicator:before { border-top-color: var(--vscode-focusBorder) !important; + border-top-width: 2px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) solid 1px !important; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:not(.checked):hover .action-label { outline: var(--vscode-contrastActiveBorder, unset) dashed 1px !important; } diff --git a/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 6f205b9d81f..84507d095dc 100644 --- a/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -354,7 +354,7 @@ type NotificationActionMetricsClassification = { id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run from a notification.' }; actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was run from a notification.' }; source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification where an action was run.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification where an action was run is silent or not.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the notification where an action was run is silent or not.' }; owner: 'bpasero'; comment: 'Tracks when actions are fired from notifcations and how they were fired.'; }; diff --git a/code/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/code/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts index 97d1d6a18c8..1a149ef7173 100644 --- a/code/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ b/code/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts @@ -17,7 +17,7 @@ export interface NotificationMetrics { export type NotificationMetricsClassification = { id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the source of the notification.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification is silent or not.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the notification is silent or not.' }; source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification.' }; owner: 'bpasero'; comment: 'Helps us gain insights to what notifications are being shown, how many, and if they are silent or not.'; diff --git a/code/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/code/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index e66dc53ad61..246532ddd02 100644 --- a/code/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/code/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -29,8 +29,8 @@ import { Event } from 'vs/base/common/event'; import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export class NotificationsListDelegate implements IListVirtualDelegate { diff --git a/code/src/vs/workbench/browser/parts/paneCompositeBar.ts b/code/src/vs/workbench/browser/parts/paneCompositeBar.ts index 8fe901376cc..94dce01b958 100644 --- a/code/src/vs/workbench/browser/parts/paneCompositeBar.ts +++ b/code/src/vs/workbench/browser/parts/paneCompositeBar.ts @@ -111,7 +111,7 @@ export class PaneCompositeBar extends Disposable { ? ViewContainerLocation.Panel : paneCompositePart.partId === Parts.AUXILIARYBAR_PART ? ViewContainerLocation.AuxiliaryBar : ViewContainerLocation.Sidebar; - this.dndHandler = new CompositeDragAndDrop(this.viewDescriptorService, this.location, + this.dndHandler = new CompositeDragAndDrop(this.viewDescriptorService, this.location, this.options.orientation, async (id: string, focus?: boolean) => { return await this.paneCompositePart.openPaneComposite(id, focus) ?? null; }, (from: string, to: string, before?: Before2D) => this.compositeBar.move(from, to, this.options.orientation === ActionsOrientation.VERTICAL ? before?.verticallyBefore : before?.horizontallyBefore), () => this.compositeBar.getCompositeBarItems(), diff --git a/code/src/vs/workbench/browser/parts/paneCompositePart.ts b/code/src/vs/workbench/browser/parts/paneCompositePart.ts index 3a1cb5382e4..96bfffb6bc2 100644 --- a/code/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/code/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -40,6 +40,12 @@ import { Composite } from 'vs/workbench/browser/composite'; import { ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +export enum CompositeBarPosition { + TOP, + TITLE, + BOTTOM +} + export interface IPaneCompositePart extends IView { readonly partId: Parts.PANEL_PART | Parts.AUXILIARYBAR_PART | Parts.SIDEBAR_PART; @@ -108,8 +114,11 @@ export abstract class AbstractPaneCompositePart extends CompositePart()); + private compositeBarPosition: CompositeBarPosition | undefined = undefined; private emptyPaneMessageElement: HTMLElement | undefined; private globalToolBar: ToolBar | undefined; @@ -238,6 +247,8 @@ export abstract class AbstractPaneCompositePart extends CompositePart this.paneFocusContextKey.set(true))); this._register(focusTracker.onDidBlur(() => this.paneFocusContextKey.set(false))); @@ -326,30 +337,107 @@ export abstract class AbstractPaneCompositePart extends CompositePart { + this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(area), e)); + })); + this.headerFooterCompositeBarDispoables.add(Gesture.addTarget(area)); + this.headerFooterCompositeBarDispoables.add(addDisposableListener(area, GestureEventType.Contextmenu, e => { + this.onCompositeBarAreaContextMenu(new StandardMouseEvent(getWindow(area), e)); + })); + + return area; + } + + private removeFooterHeaderArea(header: boolean): void { + this.headerFooterCompositeBarContainer = undefined; + this.headerFooterCompositeBarDispoables.clear(); + if (header) { + this.removeHeaderArea(); + } else { + this.removeFooterArea(); } } - protected createCompisteBar(): PaneCompositeBar { + protected createCompositeBar(): PaneCompositeBar { return this.instantiationService.createInstance(PaneCompositeBar, this.getCompositeBarOptions(), this.partId, this); } @@ -457,16 +545,20 @@ export abstract class AbstractPaneCompositePart extends CompositePart event, - getActions: () => actions, - skipTelemetry: true - }); - } + if (this.shouldShowCompositeBar() && this.getCompositeBarPosition() === CompositeBarPosition.TITLE) { + return this.onCompositeBarContextMenu(event); } else { const activePaneComposite = this.getActivePaneComposite() as PaneComposite; const activePaneCompositeActions = activePaneComposite ? activePaneComposite.getContextMenuActions() : []; @@ -512,6 +601,23 @@ export abstract class AbstractPaneCompositePart extends CompositePart event, + getActions: () => actions, + skipTelemetry: true + }); + } + } + } + protected getViewsSubmenuAction(): SubmenuAction | undefined { const viewPaneContainer = (this.getActivePaneComposite() as PaneComposite)?.getViewPaneContainer(); if (viewPaneContainer) { @@ -529,5 +635,6 @@ export abstract class AbstractPaneCompositePart extends CompositePart .content .monaco-editor, .monaco-workbench .part.panel > .content .monaco-editor .margin, .monaco-workbench .part.panel > .content .monaco-editor .monaco-editor-background { + /* THIS DOESN'T WORK ANYMORE */ background-color: var(--vscode-panel-background); } diff --git a/code/src/vs/workbench/browser/parts/panel/panelActions.ts b/code/src/vs/workbench/browser/parts/panel/panelActions.ts index 416cc1d4921..534db0283a7 100644 --- a/code/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/code/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -8,7 +8,7 @@ import { localize, localize2 } from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuId, MenuRegistry, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { IWorkbenchLayoutService, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; +import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/contextkeys'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; @@ -349,7 +349,12 @@ registerAction2(class extends Action2 { }, { id: MenuId.AuxiliaryBarTitle, group: 'navigation', - order: 2 + order: 2, + when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP) + }, { + id: MenuId.AuxiliaryBarHeader, + group: 'navigation', + when: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP) }] }); } diff --git a/code/src/vs/workbench/browser/parts/panel/panelPart.ts b/code/src/vs/workbench/browser/parts/panel/panelPart.ts index 00b10081e8a..c85e46aedce 100644 --- a/code/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/code/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -25,7 +25,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IViewDescriptorService } from 'vs/workbench/common/views'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { AbstractPaneCompositePart } from 'vs/workbench/browser/parts/paneCompositePart'; +import { AbstractPaneCompositePart, CompositeBarPosition } from 'vs/workbench/browser/parts/paneCompositePart'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IPaneCompositeBarOptions } from 'vs/workbench/browser/parts/paneCompositeBar'; @@ -175,10 +175,14 @@ export class PanelPart extends AbstractPaneCompositePart { super.layout(dimensions.width, dimensions.height, top, left); } - protected shouldShowCompositeBar(): boolean { + protected override shouldShowCompositeBar(): boolean { return true; } + protected getCompositeBarPosition(): CompositeBarPosition { + return CompositeBarPosition.TITLE; + } + toJSON(): object { return { type: Parts.PANEL_PART diff --git a/code/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/code/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 65f962e8241..0f2312e1f19 100644 --- a/code/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/code/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -60,11 +60,32 @@ height: 16px; } +.monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus { + outline: 0 !important; /* activity bar indicates focus custom */ +} + +.monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label { + border-radius: 0px; + outline-offset: 2px; +} + +.monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { + position: absolute; + left: 6px; /* place icon in center */ +} + +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked.clicked:focus .active-item-indicator:before { border-top-color: var(--vscode-activityBarTop-activeBorder) !important; } +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, +.monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:hover .action-label, .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label { color: var(--vscode-activityBarTop-foreground) !important; diff --git a/code/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/code/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 783a16cfbf6..a784caf2e0f 100644 --- a/code/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/code/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -21,7 +21,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { LayoutPriority } from 'vs/base/browser/ui/grid/grid'; import { assertIsDefined } from 'vs/base/common/types'; import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { AbstractPaneCompositePart } from 'vs/workbench/browser/parts/paneCompositePart'; +import { AbstractPaneCompositePart, CompositeBarPosition } from 'vs/workbench/browser/parts/paneCompositePart'; import { ActivityBarCompositeBar, ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; @@ -112,12 +112,19 @@ export class SidebarPart extends AbstractPaneCompositePart { } private onDidChangeActivityBarLocation(): void { - this.updateTitleArea(); + this.acitivityBarPart.hide(); + + this.updateCompositeBar(); + const id = this.getActiveComposite()?.getId(); if (id) { this.onTitleAreaUpdate(id); } - this.updateActivityBarVisiblity(); + + if (this.shouldShowActivityBar()) { + this.acitivityBarPart.show(); + } + this.rememberActivityBarVisiblePosition(); } @@ -153,7 +160,7 @@ export class SidebarPart extends AbstractPaneCompositePart { return this.layoutService.getSideBarPosition() === SideBarPosition.LEFT ? AnchorAlignment.LEFT : AnchorAlignment.RIGHT; } - protected override createCompisteBar(): ActivityBarCompositeBar { + protected override createCompositeBar(): ActivityBarCompositeBar { return this.instantiationService.createInstance(ActivityBarCompositeBar, this.getCompositeBarOptions(), this.partId, this, false); } @@ -167,7 +174,7 @@ export class SidebarPart extends AbstractPaneCompositePart { orientation: ActionsOrientation.HORIZONTAL, recomputeSizes: true, activityHoverOptions: { - position: () => HoverPosition.BELOW, + position: () => this.getCompositeBarPosition() === CompositeBarPosition.BOTTOM ? HoverPosition.ABOVE : HoverPosition.BELOW, }, fillExtraContextMenuActions: actions => { const viewsSubmenuAction = this.getViewsSubmenuAction(); @@ -178,7 +185,7 @@ export class SidebarPart extends AbstractPaneCompositePart { }, compositeSize: 0, iconSize: 16, - overflowActionSize: 44, + overflowActionSize: 30, colors: theme => ({ activeBackgroundColor: theme.getColor(SIDE_BAR_BACKGROUND), inactiveBackgroundColor: theme.getColor(SIDE_BAR_BACKGROUND), @@ -194,7 +201,8 @@ export class SidebarPart extends AbstractPaneCompositePart { } protected shouldShowCompositeBar(): boolean { - return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP; + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + return activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM; } private shouldShowActivityBar(): boolean { @@ -204,6 +212,17 @@ export class SidebarPart extends AbstractPaneCompositePart { return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.HIDDEN; } + protected getCompositeBarPosition(): CompositeBarPosition { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + switch (activityBarPosition) { + case ActivityBarPosition.TOP: return CompositeBarPosition.TOP; + case ActivityBarPosition.BOTTOM: return CompositeBarPosition.BOTTOM; + case ActivityBarPosition.HIDDEN: + case ActivityBarPosition.DEFAULT: // noop + default: return CompositeBarPosition.TITLE; + } + } + private rememberActivityBarVisiblePosition(): void { const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); if (activityBarPosition !== ActivityBarPosition.HIDDEN) { @@ -214,16 +233,9 @@ export class SidebarPart extends AbstractPaneCompositePart { private getRememberedActivityBarVisiblePosition(): ActivityBarPosition { const activityBarPosition = this.storageService.get(LayoutSettings.ACTIVITY_BAR_LOCATION, StorageScope.PROFILE); switch (activityBarPosition) { - case ActivityBarPosition.SIDE: return ActivityBarPosition.SIDE; - default: return ActivityBarPosition.TOP; - } - } - - private updateActivityBarVisiblity(): void { - if (this.shouldShowActivityBar()) { - this.acitivityBarPart.show(); - } else { - this.acitivityBarPart.hide(); + case ActivityBarPosition.TOP: return ActivityBarPosition.TOP; + case ActivityBarPosition.BOTTOM: return ActivityBarPosition.BOTTOM; + default: return ActivityBarPosition.DEFAULT; } } diff --git a/code/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/code/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index e18622523c5..76af9a42b7f 100644 --- a/code/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/code/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -21,9 +21,9 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; export class StatusbarEntryItem extends Disposable { @@ -73,7 +73,7 @@ export class StatusbarEntryItem extends Disposable { this._register(Gesture.addTarget(this.labelContainer)); // enable touch // Label (with support for progress) - this.label = new StatusBarCodiconLabel(this.labelContainer); + this.label = this._register(new StatusBarCodiconLabel(this.labelContainer)); this.container.appendChild(this.labelContainer); // Beak Container diff --git a/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 664a333bb16..06548d13508 100644 --- a/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -338,7 +338,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.element = parent; // Track focus within container - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); StatusBarFocused.bindTo(scopedContextKeyService).set(true); // Left items container diff --git a/code/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/code/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 72eeea0b590..54eded7aab3 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -5,9 +5,9 @@ import { isActiveDocument, reset } from 'vs/base/browser/dom'; import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; diff --git a/code/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/code/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index a3d09742994..1453c7d8eeb 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -189,7 +189,7 @@ export abstract class MenubarControl extends Disposable { this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); // Listen to update service - this.updateService.onStateChange(() => this.onUpdateStateChange()); + this._register(this.updateService.onStateChange(() => this.onUpdateStateChange())); // Listen for changes in recently opened menu this._register(this.workspacesService.onDidChangeRecentlyOpened(() => { this.onDidChangeRecentlyOpened(); })); @@ -474,7 +474,7 @@ export class CustomMenubarControl extends MenubarControl { return new Action('update.downloading', localize('DownloadingUpdate', "Downloading Update..."), undefined, false); case StateType.Downloaded: - return new Action('update.install', localize({ key: 'installUpdate...', comment: ['&& denotes a mnemonic'] }, "Install &&Update..."), undefined, true, () => + return isMacintosh ? null : new Action('update.install', localize({ key: 'installUpdate...', comment: ['&& denotes a mnemonic'] }, "Install &&Update..."), undefined, true, () => this.updateService.applyUpdate()); case StateType.Updating: diff --git a/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index 8b9ec83840d..632bf55e2e2 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -116,7 +116,8 @@ class ToggleCustomTitleBar extends Action2 { ContextKeyExpr.equals('config.workbench.layoutControl.enabled', false), ContextKeyExpr.equals('config.window.commandCenter', false), ContextKeyExpr.notEquals('config.workbench.editor.editorActionsLocation', 'titleBar'), - ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'top') + ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'top'), + ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'bottom') )?.negate() ), IsMainWindowFullscreenContext diff --git a/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 176ac265172..6439d953ac1 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -49,12 +49,12 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { EditorCommandsContextActionRunner } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { IEditorCommandsContext, IEditorPartOptionsChangeEvent, IToolbarActions } from 'vs/workbench/common/editor'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; import { ACCOUNTS_ACTIVITY_TILE_ACTION, GLOBAL_ACTIVITY_TITLE_ACTION } from 'vs/workbench/browser/parts/titlebar/titlebarActions'; import { IView } from 'vs/base/browser/ui/grid/grid'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; export interface ITitleVariable { readonly name: string; @@ -111,7 +111,7 @@ export class BrowserTitleService extends MultiWindowParts i // Focus action const that = this; - registerAction2(class FocusTitleBar extends Action2 { + this._register(registerAction2(class FocusTitleBar extends Action2 { constructor() { super({ @@ -125,7 +125,7 @@ export class BrowserTitleService extends MultiWindowParts i run(): void { that.getPartByDocument(getActiveDocument()).focus(); } - }); + })); } //#region Auxiliary Titlebar Parts @@ -258,7 +258,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { constructor( id: string, - targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService protected readonly configurationService: IConfigurationService, @@ -282,7 +282,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.windowTitle = this._register(instantiationService.createInstance(WindowTitle, targetWindow, editorGroupsContainer)); - this.hoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + this.hoverDelegate = this._register(createInstantHoverDelegate()); this.registerListeners(getWindowId(targetWindow)); } @@ -728,7 +728,8 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } private get activityActionsEnabled(): boolean { - return !this.isAuxiliary && this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP; + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + return !this.isAuxiliary && (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM); } get hasZoomableElements(): boolean { diff --git a/code/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/code/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 3ec39eeafc2..e025f5c4d6c 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -27,6 +27,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getWindowById } from 'vs/base/browser/dom'; +import { CodeWindow } from 'vs/base/browser/window'; const enum WindowSettingNames { titleSeparator = 'window.titleSeparator', @@ -79,8 +81,10 @@ export class WindowTitle extends Disposable { private readonly editorService: IEditorService; + private readonly windowId: number; + constructor( - private readonly targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IConfigurationService protected readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -95,6 +99,7 @@ export class WindowTitle extends Disposable { super(); this.editorService = editorService.createScoped(editorGroupsContainer, this._store); + this.windowId = targetWindow.vscodeWindowId; this.updateTitleIncludesFocusedView(); this.registerListeners(); @@ -177,7 +182,8 @@ export class WindowTitle extends Disposable { nativeTitle = this.productService.nameLong; } - if (!this.targetWindow.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { + const window = getWindowById(this.windowId, true).window; + if (!window.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { // TODO@electron macOS: if we set a window title for // the first time and it matches the one we set in // `windowImpl.ts` somehow the window does not appear @@ -185,10 +191,10 @@ export class WindowTitle extends Disposable { // briefly to something different to ensure macOS // recognizes we have a window. // See: https://github.com/microsoft/vscode/issues/191288 - this.targetWindow.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; + window.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; } - this.targetWindow.document.title = nativeTitle; + window.document.title = nativeTitle; this.title = title; this.onDidChangeEmitter.fire(); diff --git a/code/src/vs/workbench/browser/parts/views/checkbox.ts b/code/src/vs/workbench/browser/parts/views/checkbox.ts index 849966bbe33..62058fd74fc 100644 --- a/code/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/code/src/vs/workbench/browser/parts/views/checkbox.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; @@ -81,8 +81,7 @@ export class TreeItemCheckbox extends Disposable { private setHover(checkbox: ITreeItemCheckboxState) { if (this.toggle) { if (!this.hover) { - this.hover = setupCustomHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox)); - this._register(this.hover); + this.hover = this._register(setupCustomHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox))); } else { this.hover.update(checkbox.tooltip); } diff --git a/code/src/vs/workbench/browser/parts/views/media/views.css b/code/src/vs/workbench/browser/parts/views/media/views.css index 15c4a1aae00..130fd60fe85 100644 --- a/code/src/vs/workbench/browser/parts/views/media/views.css +++ b/code/src/vs/workbench/browser/parts/views/media/views.css @@ -268,7 +268,7 @@ } .viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls > .viewpane-filter-badge { - margin: 4px 0px; + margin: 4px 2px 4px 0px; padding: 0px 8px; border-radius: 2px; } @@ -278,10 +278,6 @@ display: none; } -.viewpane-filter > .viewpane-filter-controls > .monaco-action-bar .action-item .action-label.codicon.filter { - padding: 2px; -} - .panel > .title .monaco-action-bar .action-item.viewpane-filter-container { max-width: 400px; min-width: 150px; diff --git a/code/src/vs/workbench/browser/parts/views/treeView.ts b/code/src/vs/workbench/browser/parts/views/treeView.ts index 0eb478a3544..661638fa561 100644 --- a/code/src/vs/workbench/browser/parts/views/treeView.ts +++ b/code/src/vs/workbench/browser/parts/views/treeView.ts @@ -8,8 +8,8 @@ import * as DOM from 'vs/base/browser/dom'; import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { ITooltipMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ITooltipMarkdownString } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData, ListViewTargetSector } from 'vs/base/browser/ui/list/listView'; import { IAsyncDataSource, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, ITreeNode, ITreeRenderer, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; @@ -767,7 +767,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { let command = element?.command; if (element && !command) { if ((element instanceof ResolvableTreeItem) && element.hasResolve) { - await element.resolve(new CancellationTokenSource().token); + await element.resolve(CancellationToken.None); command = element.command; } } @@ -1106,7 +1106,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer this.rerender())); this._register(this.themeService.onDidColorThemeChange(() => this.rerender())); this._register(checkboxStateHandler.onDidChangeCheckboxState(items => { diff --git a/code/src/vs/workbench/browser/parts/views/viewPane.ts b/code/src/vs/workbench/browser/parts/views/viewPane.ts index 9cc1b35c354..294c69b1448 100644 --- a/code/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/code/src/vs/workbench/browser/parts/views/viewPane.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { asCssVariable, foreground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault, focusWindow } from 'vs/base/browser/dom'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault } from 'vs/base/browser/dom'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -47,8 +47,8 @@ import { FilterWidget, IFilterWidgetOptions } from 'vs/workbench/browser/parts/v import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export enum ViewPaneShowActions { /** Show the actions when the view is hovered. This is the default behavior. */ @@ -66,6 +66,8 @@ export interface IViewPaneOptions extends IPaneOptions { readonly showActions?: ViewPaneShowActions; readonly titleMenuId?: MenuId; readonly donotForwardArgs?: boolean; + // The title of the container pane when it is merged with the view container + readonly singleViewPaneContainerTitle?: string; } export interface IFilterViewPaneOptions extends IViewPaneOptions { @@ -333,6 +335,11 @@ export abstract class ViewPane extends Pane implements IView { return this._titleDescription; } + private _singleViewPaneContainerTitle: string | undefined; + public get singleViewPaneContainerTitle(): string | undefined { + return this._singleViewPaneContainerTitle; + } + readonly menuActions: CompositeMenuActions; private progressBar!: ProgressBar; @@ -342,7 +349,9 @@ export abstract class ViewPane extends Pane implements IView { private readonly showActions: ViewPaneShowActions; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; + private titleContainerHover?: ICustomHover; private titleDescriptionContainer?: HTMLElement; + private titleDescriptionContainerHover?: ICustomHover; private iconContainer?: HTMLElement; private iconContainerHover?: ICustomHover; protected twistiesContainer?: HTMLElement; @@ -367,6 +376,7 @@ export abstract class ViewPane extends Pane implements IView { this.id = options.id; this._title = options.title; this._titleDescription = options.titleDescription; + this._singleViewPaneContainerTitle = options.singleViewPaneContainerTitle; this.showActions = options.showActions ?? ViewPaneShowActions.Default; this.scopedContextKeyService = this._register(contextKeyService.createScoped(this.element)); @@ -523,7 +533,7 @@ export abstract class ViewPane extends Pane implements IView { const calculatedTitle = this.calculateTitle(title); this.titleContainer = append(container, $('h3.title', {}, calculatedTitle)); - setupCustomHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle); + this.titleContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle)); if (this._titleDescription) { this.setTitleDescription(this._titleDescription); @@ -537,7 +547,7 @@ export abstract class ViewPane extends Pane implements IView { const calculatedTitle = this.calculateTitle(title); if (this.titleContainer) { this.titleContainer.textContent = calculatedTitle; - this.titleContainer.setAttribute('title', calculatedTitle); + this.titleContainerHover?.update(calculatedTitle); } if (this.iconContainer) { @@ -552,10 +562,11 @@ export abstract class ViewPane extends Pane implements IView { private setTitleDescription(description: string | undefined) { if (this.titleDescriptionContainer) { this.titleDescriptionContainer.textContent = description ?? ''; - this.titleDescriptionContainer.setAttribute('title', description ?? ''); + this.titleDescriptionContainerHover?.update(description ?? ''); } else if (description && this.titleContainer) { - this.titleDescriptionContainer = after(this.titleContainer, $('span.description', { title: description }, description)); + this.titleDescriptionContainer = after(this.titleContainer, $('span.description', {}, description)); + this.titleDescriptionContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.titleDescriptionContainer, description)); } } @@ -600,12 +611,12 @@ export abstract class ViewPane extends Pane implements IView { if (this.progressIndicator === undefined) { const that = this; - this.progressIndicator = new ScopedProgressIndicator(assertIsDefined(this.progressBar), new class extends AbstractProgressScope { + this.progressIndicator = this._register(new ScopedProgressIndicator(assertIsDefined(this.progressBar), new class extends AbstractProgressScope { constructor() { super(that.id, that.isBodyVisible()); this._register(that.onDidChangeBodyVisibility(isVisible => isVisible ? this.onScopeOpened(that.id) : this.onScopeClosed(that.id))); } - }()); + }())); } return this.progressIndicator; } @@ -627,8 +638,6 @@ export abstract class ViewPane extends Pane implements IView { } focus(): void { - focusWindow(this.element); - if (this.viewWelcomeController.enabled) { this.viewWelcomeController.focus(); } else if (this.element) { diff --git a/code/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/code/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index f575fa95242..67b7f28268f 100644 --- a/code/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/code/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -20,7 +20,7 @@ import * as nls from 'vs/nls'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Action2, IAction2Options, IMenuService, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -39,7 +39,7 @@ import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerMo import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, LayoutSettings, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; export const ViewsSubMenu = new MenuId('Views'); @@ -47,7 +47,6 @@ MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { submenu: ViewsSubMenu, title: nls.localize('views', "Views"), order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP)), }); export interface IViewPaneContainerOptions extends IPaneViewOptions { @@ -560,10 +559,16 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const containerTitle = this.viewContainerModel.title; if (this.isViewMergedWithContainer()) { + const singleViewPaneContainerTitle = this.paneItems[0].pane.singleViewPaneContainerTitle; + if (singleViewPaneContainerTitle) { + return singleViewPaneContainerTitle; + } + const paneItemTitle = this.paneItems[0].pane.title; if (containerTitle === paneItemTitle) { - return this.paneItems[0].pane.title; + return paneItemTitle; } + return paneItemTitle ? `${containerTitle}: ${paneItemTitle}` : containerTitle; } @@ -780,7 +785,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { id: viewDescriptor.id, title: viewDescriptor.name.value, fromExtensionId: (viewDescriptor as Partial).extensionId, - expanded: !collapsed + expanded: !collapsed, + singleViewPaneContainerTitle: viewDescriptor.singleViewPaneContainerTitle, }); pane.render(); @@ -1091,11 +1097,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } isViewMergedWithContainer(): boolean { - const location = this.viewDescriptorService.getViewContainerLocation(this.viewContainer); - // Do not merge views in side bar when activity bar is on top because the view title is not shown - if (location === ViewContainerLocation.Sidebar && !this.layoutService.isVisible(Parts.ACTIVITYBAR_PART) && this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.TOP) { - return false; - } if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) { return false; } diff --git a/code/src/vs/workbench/browser/quickaccess.ts b/code/src/vs/workbench/browser/quickaccess.ts index 6b9e07abe7a..aec2065963d 100644 --- a/code/src/vs/workbench/browser/quickaccess.ts +++ b/code/src/vs/workbench/browser/quickaccess.ts @@ -8,12 +8,14 @@ import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/con import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { Disposable } from 'vs/base/common/lifecycle'; import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorViewState, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; +import { IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import { IUntitledTextResourceEditorInput, IUntypedEditorInput, GroupIdentifier, IEditorPane } from 'vs/workbench/common/editor'; export const inQuickPickContextKeyValue = 'inQuickOpen'; export const InQuickPickContextKey = new RawContextKey(inQuickPickContextKeyValue, false, localize('inQuickOpen', "Whether keyboard focus is inside the quick open control")); @@ -51,14 +53,21 @@ export function getQuickNavigateHandler(id: string, next?: boolean): ICommandHan quickInputService.navigate(!!next, quickNavigate); }; } -export class EditorViewState { +export class PickerEditorState extends Disposable { private _editorViewState: { editor: EditorInput; group: IEditorGroup; state: ICodeEditorViewState | IDiffEditorViewState | undefined; } | undefined = undefined; - constructor(private readonly editorService: IEditorService) { } + private readonly openedTransientEditors = new Set(); // editors that were opened between set and restore + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService + ) { + super(); + } set(): void { if (this._editorViewState) { @@ -73,27 +82,55 @@ export class EditorViewState { state: getIEditor(activeEditorPane.getControl())?.saveViewState() ?? undefined, }; } + } - async restore(shouldCloseCurrEditor = false): Promise { + /** + * Open a transient editor such that it may be closed when the state is restored. + * Note that, when the state is restored, if the editor is no longer transient, it will not be closed. + */ + async openTransientEditor(editor: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IUntypedEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise { + editor.options = { ...editor.options, transient: true }; + + const editorPane = await this.editorService.openEditor(editor, group); + if (editorPane?.input && editorPane.input !== this._editorViewState?.editor && editorPane.group.isTransient(editorPane.input)) { + this.openedTransientEditors.add(editorPane.input); + } + + return editorPane; + } + + async restore(): Promise { if (this._editorViewState) { - const options: IEditorOptions = { - viewState: this._editorViewState.state, - preserveFocus: true /* import to not close the picker as a result */ - }; - if (shouldCloseCurrEditor) { - const activeEditorPane = this.editorService.activeEditorPane; - const currEditor = activeEditorPane?.input; - if (currEditor && currEditor !== this._editorViewState.editor && activeEditorPane?.group.isPinned(currEditor) !== true) { - await activeEditorPane.group.closeEditor(currEditor); + for (const editor of this.openedTransientEditors) { + if (editor.isDirty()) { + continue; + } + + for (const group of this.editorGroupsService.groups) { + if (group.isTransient(editor)) { + await group.closeEditor(editor, { preserveFocus: true }); + } } } - await this._editorViewState.group.openEditor(this._editorViewState.editor, options); + await this._editorViewState.group.openEditor(this._editorViewState.editor, { + viewState: this._editorViewState.state, + preserveFocus: true // important to not close the picker as a result + }); + + this.reset(); } } reset() { this._editorViewState = undefined; + this.openedTransientEditors.clear(); + } + + override dispose(): void { + super.dispose(); + + this.reset(); } } diff --git a/code/src/vs/workbench/browser/style.ts b/code/src/vs/workbench/browser/style.ts index 23f2e2bac99..8fab9bc5b71 100644 --- a/code/src/vs/workbench/browser/style.ts +++ b/code/src/vs/workbench/browser/style.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/style'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; -import { isWeb, isIOS, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isWeb, isIOS } from 'vs/base/common/platform'; import { createMetaElement } from 'vs/base/browser/dom'; import { isSafari, isStandalone } from 'vs/base/browser/browser'; import { selectionBackground } from 'vs/platform/theme/common/colorRegistry'; @@ -60,13 +60,3 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`body { background-color: ${workbenchBackground}; }`); } }); - -/** - * The best font-family to be used in CSS based on the platform: - * - Windows: Segoe preferred, fallback to sans-serif - * - macOS: standard system font, fallback to sans-serif - * - Linux: standard system font preferred, fallback to Ubuntu fonts - * - * Note: this currently does not adjust for different locales. - */ -export const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif'; diff --git a/code/src/vs/workbench/browser/web.main.ts b/code/src/vs/workbench/browser/web.main.ts index b36400ec9f1..6250a90d6c1 100644 --- a/code/src/vs/workbench/browser/web.main.ts +++ b/code/src/vs/workbench/browser/web.main.ts @@ -476,7 +476,7 @@ export class BrowserMain extends Disposable { } private registerDeveloperActions(provider: IndexedDBFileSystemProvider): void { - registerAction2(class ResetUserDataAction extends Action2 { + this._register(registerAction2(class ResetUserDataAction extends Action2 { constructor() { super({ id: 'workbench.action.resetUserData', @@ -511,7 +511,7 @@ export class BrowserMain extends Disposable { hostService.reload(); } - }); + })); } private async createStorageService(workspace: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService): Promise { diff --git a/code/src/vs/workbench/browser/window.ts b/code/src/vs/workbench/browser/window.ts index 17354e5f403..e964af6543e 100644 --- a/code/src/vs/workbench/browser/window.ts +++ b/code/src/vs/workbench/browser/window.ts @@ -30,6 +30,7 @@ import { registerWindowDriver } from 'vs/workbench/services/driver/browser/drive import { CodeWindow, isAuxiliaryWindow, mainWindow } from 'vs/base/browser/window'; import { createSingleCallFunction } from 'vs/base/common/functional'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export abstract class BaseWindow extends Disposable { @@ -39,7 +40,8 @@ export abstract class BaseWindow extends Disposable { constructor( targetWindow: CodeWindow, dom = { getWindowsCount, getWindows }, /* for testing */ - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); @@ -52,31 +54,54 @@ export abstract class BaseWindow extends Disposable { //#region focus handling in multi-window applications protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void { - const originalFocus = HTMLElement.prototype.focus; + const originalFocus = targetWindow.HTMLElement.prototype.focus; + const that = this; targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // If the active focused window is not the same as the - // window of the element to focus, make sure to focus - // that window first before focusing the element. - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus()) { - const elementWindow = getWindow(this); - if (activeWindow !== elementWindow) { - elementWindow.focus(); - } - } + // Ensure the window the element belongs to is focused + // in scenarios where auxiliary windows are present + that.onElementFocus(getWindow(this)); // Pass to original focus() method originalFocus.apply(this, [options]); }; } + private onElementFocus(targetWindow: CodeWindow): void { + const activeWindow = getActiveWindow(); + if (activeWindow !== targetWindow && activeWindow.document.hasFocus()) { + + // Call original focus() + targetWindow.focus(); + + // In Electron, `window.focus()` fails to bring the window + // to the front if multiple windows exist in the same process + // group (floating windows). As such, we ask the host service + // to focus the window which can take care of bringin the + // window to the front. + // + // To minimise disruption by bringing windows to the front + // by accident, we only do this if the window is not already + // focused and the active window is not the target window + // but has focus. This is an indication that multiple windows + // are opened in the same process group while the target window + // is not focused. + + if ( + !this.environmentService.extensionTestsLocationURI && + !targetWindow.document.hasFocus() + ) { + this.hostService.focus(targetWindow); + } + } + } + //#endregion //#region timeout handling in multi-window applications - private enableMultiWindowAwareTimeout(targetWindow: Window, dom = { getWindowsCount, getWindows }): void { + protected enableMultiWindowAwareTimeout(targetWindow: Window, dom = { getWindowsCount, getWindows }): void { // Override `setTimeout` and `clearTimeout` on the provided window to make // sure timeouts are dispatched to all opened windows. Some browsers may decide @@ -186,12 +211,12 @@ export class BrowserWindow extends BaseWindow { @IDialogService private readonly dialogService: IDialogService, @ILabelService private readonly labelService: ILabelService, @IProductService private readonly productService: IProductService, - @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, + @IBrowserWorkbenchEnvironmentService private readonly browserEnvironmentService: IBrowserWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHostService hostService: IHostService ) { - super(mainWindow, undefined, hostService); + super(mainWindow, undefined, hostService, browserEnvironmentService); this.registerListeners(); this.create(); @@ -288,8 +313,8 @@ export class BrowserWindow extends BaseWindow { this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { let isAllowedOpener = false; - if (this.environmentService.options?.openerAllowedExternalUrlPrefixes) { - for (const trustedPopupPrefix of this.environmentService.options.openerAllowedExternalUrlPrefixes) { + if (this.browserEnvironmentService.options?.openerAllowedExternalUrlPrefixes) { + for (const trustedPopupPrefix of this.browserEnvironmentService.options.openerAllowedExternalUrlPrefixes) { if (href.startsWith(trustedPopupPrefix)) { isAllowedOpener = true; break; diff --git a/code/src/vs/workbench/browser/workbench.contribution.ts b/code/src/vs/workbench/browser/workbench.contribution.ts index b715436c0d4..6ed828c45d6 100644 --- a/code/src/vs/workbench/browser/workbench.contribution.ts +++ b/code/src/vs/workbench/browser/workbench.contribution.ts @@ -497,13 +497,14 @@ const registry = Registry.as(ConfigurationExtensions.Con }, [LayoutSettings.ACTIVITY_BAR_LOCATION]: { 'type': 'string', - 'enum': ['side', 'top', 'hidden'], - 'default': 'side', - 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `side` or `top` of the Primary Side Bar or `hidden`."), + 'enum': ['default', 'top', 'bottom', 'hidden'], + 'default': 'default', + 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `default` or `top` / `bottom` of the Primary and Secondary Side Bar or `hidden`."), 'enumDescriptions': [ - localize('workbench.activityBar.location.side', "Show the Activity Bar to the side of the Primary Side Bar."), - localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary Side Bar."), - localize('workbench.activityBar.location.hide', "Hide the Activity Bar.") + localize('workbench.activityBar.location.default', "Show the Activity Bar of the Primary Side Bar on the side."), + localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bar."), + localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bar."), + localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bar.") ], }, 'workbench.activityBar.iconClickBehavior': { @@ -810,6 +811,17 @@ Registry.as(Extensions.ConfigurationMigration) } }]); +Registry.as(Extensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: LayoutSettings.ACTIVITY_BAR_LOCATION, migrateFn: (value: any) => { + const results: ConfigurationKeyValuePairs = []; + if (value === 'side') { + results.push([LayoutSettings.ACTIVITY_BAR_LOCATION, { value: ActivityBarPosition.DEFAULT }]); + } + return results; + } + }]); + Registry.as(Extensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'workbench.editor.doubleClickTabToToggleEditorGroupSizes', migrateFn: (value: any) => { diff --git a/code/src/vs/workbench/browser/workbench.ts b/code/src/vs/workbench/browser/workbench.ts index 8a311d4bb0e..c7302a6be67 100644 --- a/code/src/vs/workbench/browser/workbench.ts +++ b/code/src/vs/workbench/browser/workbench.ts @@ -44,7 +44,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mainWindow } from 'vs/base/browser/window'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; import { WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; -import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface IWorkbenchOptions { @@ -141,7 +141,7 @@ export class Workbench extends Layout { try { // Configure emitter leak warning threshold - setGlobalLeakWarningThreshold(175); + this._register(setGlobalLeakWarningThreshold(175)); // Services const instantiationService = this.initServices(this.serviceCollection); diff --git a/code/src/vs/workbench/common/comments.ts b/code/src/vs/workbench/common/comments.ts new file mode 100644 index 00000000000..038819d8f99 --- /dev/null +++ b/code/src/vs/workbench/common/comments.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { CommentThread } from 'vs/editor/common/languages'; + +export interface MarshalledCommentThread { + $mid: MarshalledId.CommentThread; + commentControlHandle: number; + commentThreadHandle: number; +} + +export interface MarshalledCommentThreadInternal extends MarshalledCommentThread { + thread: CommentThread; +} diff --git a/code/src/vs/workbench/common/component.ts b/code/src/vs/workbench/common/component.ts index 6c25dc9d977..f8dd0115412 100644 --- a/code/src/vs/workbench/common/component.ts +++ b/code/src/vs/workbench/common/component.ts @@ -20,7 +20,6 @@ export class Component extends Themable { ) { super(themeService); - this.id = id; this.memento = new Memento(this.id, storageService); this._register(storageService.onWillSaveState(() => { diff --git a/code/src/vs/workbench/common/contextkeys.ts b/code/src/vs/workbench/common/contextkeys.ts index a6464aeacd9..612f006a88c 100644 --- a/code/src/vs/workbench/common/contextkeys.ts +++ b/code/src/vs/workbench/common/contextkeys.ts @@ -52,7 +52,7 @@ export const ActiveEditorFirstInGroupContext = new RawContextKey('activ export const ActiveEditorLastInGroupContext = new RawContextKey('activeEditorIsLastInGroup', false, localize('activeEditorIsLastInGroup', "Whether the active editor is the last one in its group")); export const ActiveEditorStickyContext = new RawContextKey('activeEditorIsPinned', false, localize('activeEditorIsPinned', "Whether the active editor is pinned")); export const ActiveEditorReadonlyContext = new RawContextKey('activeEditorIsReadonly', false, localize('activeEditorIsReadonly', "Whether the active editor is read-only")); -export const ActiveCompareEditorOriginalWriteableContext = new RawContextKey('activeCompareEditorOriginalWritable', false, localize('activeCompareEditorOriginalWritable', "Whether the active compare editor has a writable original side")); +export const ActiveCompareEditorCanSwapContext = new RawContextKey('activeCompareEditorCanSwap', false, localize('activeCompareEditorCanSwap', "Whether the active compare editor can swap sides")); export const ActiveEditorCanToggleReadonlyContext = new RawContextKey('activeEditorCanToggleReadonly', true, localize('activeEditorCanToggleReadonly', "Whether the active editor can toggle between being read-only or writeable")); export const ActiveEditorCanRevertContext = new RawContextKey('activeEditorCanRevert', false, localize('activeEditorCanRevert', "Whether the active editor can revert")); export const ActiveEditorCanSplitInGroupContext = new RawContextKey('activeEditorCanSplitInGroup', true); diff --git a/code/src/vs/workbench/common/contributions.ts b/code/src/vs/workbench/common/contributions.ts index b96df678196..aaf1452c25a 100644 --- a/code/src/vs/workbench/common/contributions.ts +++ b/code/src/vs/workbench/common/contributions.ts @@ -385,7 +385,7 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb } } - if (typeof contribution.id === 'string' || !environmentService.isBuilt /* only log out of sources where we have good ctor names (TODO@bpasero remove when adopted IDs) */) { + if (typeof contribution.id === 'string' || !environmentService.isBuilt /* only log out of sources where we have good ctor names */) { const time = Date.now() - now; if (time > (phase < LifecyclePhase.Restored ? WorkbenchContributionsRegistry.BLOCK_BEFORE_RESTORE_WARN_THRESHOLD : WorkbenchContributionsRegistry.BLOCK_AFTER_RESTORE_WARN_THRESHOLD)) { logService.warn(`Creation of workbench contribution '${contribution.id ?? contribution.ctor.name}' took ${time}ms.`); diff --git a/code/src/vs/workbench/common/editor.ts b/code/src/vs/workbench/common/editor.ts index c6986bd0f57..bd2c42d8510 100644 --- a/code/src/vs/workbench/common/editor.ts +++ b/code/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorDescriptor { /** * Instantiates the editor pane using the provided services. */ - instantiate(instantiationService: IInstantiationService): T; + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): T; /** * Whether the descriptor is for the provided editor pane. @@ -106,6 +106,11 @@ export interface IEditorPane extends IComposite { */ readonly onDidChangeSelection?: Event; + /** + * An optional event to notify when the editor inside the pane scrolled + */ + readonly onDidChangeScroll?: Event; + /** * The assigned input of this editor. */ @@ -119,7 +124,7 @@ export interface IEditorPane extends IComposite { /** * The assigned group this editor is showing in. */ - readonly group: IEditorGroup | undefined; + readonly group: IEditorGroup; /** * The minimum width of this editor. @@ -182,6 +187,22 @@ export interface IEditorPane extends IComposite { */ getSelection?(): IEditorPaneSelection | undefined; + /** + * An optional method to return the current scroll position + * of an editor inside the pane. + * + * Clients of this method will typically react to the + * `onDidChangeScroll` event to receive the current + * scroll position as needed. + */ + getScrollPosition?(): IEditorPaneScrollPosition; + + /** + * An optional method to set the current scroll position + * of an editor inside the pane. + */ + setScrollPosition?(scrollPosition: IEditorPaneScrollPosition): void; + /** * Finds out if this editor is visible or not. */ @@ -305,6 +326,29 @@ export function isEditorPaneWithSelection(editorPane: IEditorPane | undefined): return !!candidate && typeof candidate.getSelection === 'function' && !!candidate.onDidChangeSelection; } +export interface IEditorPaneWithScrolling extends IEditorPane { + + readonly onDidChangeScroll: Event; + + getScrollPosition(): IEditorPaneScrollPosition; + + setScrollPosition(position: IEditorPaneScrollPosition): void; +} + +export function isEditorPaneWithScrolling(editorPane: IEditorPane | undefined): editorPane is IEditorPaneWithScrolling { + const candidate = editorPane as IEditorPaneWithScrolling | undefined; + + return !!candidate && typeof candidate.getScrollPosition === 'function' && typeof candidate.setScrollPosition === 'function' && !!candidate.onDidChangeScroll; +} + +/** + * Scroll position of a pane + */ +export interface IEditorPaneScrollPosition { + readonly scrollTop: number; + readonly scrollLeft?: number; +} + /** * Try to retrieve the view state for the editor pane that * has the provided editor input opened, if at all. @@ -327,7 +371,6 @@ export function findViewStateForEditor(input: EditorInput, group: GroupIdentifie */ export interface IVisibleEditorPane extends IEditorPane { readonly input: EditorInput; - readonly group: IEditorGroup; } /** @@ -564,7 +607,7 @@ export function isResourceDiffEditorInput(editor: unknown): editor is IResourceD return candidate?.original !== undefined && candidate.modified !== undefined; } -export function isResourceDiffListEditorInput(editor: unknown): editor is IResourceMultiDiffEditorInput { +export function isResourceMultiDiffEditorInput(editor: unknown): editor is IResourceMultiDiffEditorInput { if (isEditorInput(editor)) { return false; // make sure to not accidentally match on typed editor inputs } @@ -790,13 +833,7 @@ export const enum EditorInputCapabilities { * Signals that the editor cannot be in a dirty state * and may still have unsaved changes */ - Scratchpad = 1 << 9, - - /** - * Signals that the editor does not support opening in - * auxiliary windows yet. - */ - AuxWindowUnsupported = 1 << 10 + Scratchpad = 1 << 9 } export type IUntypedEditorInput = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput | IResourceMultiDiffEditorInput | IResourceSideBySideEditorInput | IResourceMergeEditorInput; @@ -1133,6 +1170,7 @@ export const enum GroupModelChangeKind { EDITOR_LABEL, EDITOR_CAPABILITIES, EDITOR_PIN, + EDITOR_TRANSIENT, EDITOR_STICKY, EDITOR_DIRTY, EDITOR_WILL_DISPOSE @@ -1310,7 +1348,7 @@ class EditorResourceAccessorImpl { } } - if (isResourceDiffEditorInput(editor) || isResourceDiffListEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { + if (isResourceDiffEditorInput(editor) || isResourceMultiDiffEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { return undefined; } @@ -1379,7 +1417,7 @@ class EditorResourceAccessorImpl { } } - if (isResourceDiffEditorInput(editor) || isResourceDiffListEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { + if (isResourceDiffEditorInput(editor) || isResourceMultiDiffEditorInput(editor) || isResourceSideBySideEditorInput(editor) || isResourceMergeEditorInput(editor)) { return undefined; } diff --git a/code/src/vs/workbench/common/editor/editorGroupModel.ts b/code/src/vs/workbench/common/editor/editorGroupModel.ts index 8a018234256..60047f630e3 100644 --- a/code/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/code/src/vs/workbench/common/editor/editorGroupModel.ts @@ -22,7 +22,8 @@ const EditorOpenPositioning = { export interface IEditorOpenOptions { readonly pinned?: boolean; - sticky?: boolean; + readonly sticky?: boolean; + readonly transient?: boolean; active?: boolean; readonly index?: number; readonly supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; @@ -180,6 +181,7 @@ export interface IReadonlyEditorGroupModel { isActive(editor: EditorInput | IUntypedEditorInput): boolean; isPinned(editorOrIndex: EditorInput | number): boolean; isSticky(editorOrIndex: EditorInput | number): boolean; + isTransient(editorOrIndex: EditorInput | number): boolean; isFirst(editor: EditorInput, editors?: EditorInput[]): boolean; isLast(editor: EditorInput, editors?: EditorInput[]): boolean; findEditor(editor: EditorInput | null, options?: IMatchEditorOptions): [EditorInput, number /* index */] | undefined; @@ -217,6 +219,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private preview: EditorInput | null = null; // editor in preview state private active: EditorInput | null = null; // editor in active state private sticky = -1; // index of first editor in sticky state + private transient = new Set(); // editors in transient state private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined; private focusRecentEditorAfterClose: boolean | undefined; @@ -295,6 +298,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { openEditor(candidate: EditorInput, options?: IEditorOpenOptions): IEditorOpenResult { const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); const makePinned = options?.pinned || options?.sticky; + const makeTransient = !!options?.transient; const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); const existingEditorAndIndex = this.findEditor(candidate, options); @@ -365,6 +369,11 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.splice(targetIndex, false, newEditor); } + // Handle transient + if (makeTransient) { + this.doSetTransient(newEditor, targetIndex, true); + } + // Handle preview if (!makePinned) { @@ -407,6 +416,9 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { else { const [existingEditor, existingEditorIndex] = existingEditorAndIndex; + // Update transient (existing editors do not turn transient if they were not before) + this.doSetTransient(existingEditor, existingEditorIndex, makeTransient === false ? false : this.isTransient(existingEditor)); + // Pin it if (makePinned) { this.doPin(existingEditor, existingEditorIndex); @@ -563,6 +575,9 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.preview = null; } + // Remove from transient + this.transient.delete(editor); + // Remove from arrays this.splice(index, true); @@ -711,6 +726,9 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return; // can only pin a preview editor } + // Clear Transient + this.setTransient(editor, false); + // Convert the preview editor to be a pinned editor this.preview = null; @@ -860,6 +878,62 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return index <= this.sticky; } + setTransient(candidate: EditorInput, transient: boolean): EditorInput | undefined { + if (!transient && this.transient.size === 0) { + return; // no transient editor + } + + const res = this.findEditor(candidate); + if (!res) { + return; // not found + } + + const [editor, editorIndex] = res; + + this.doSetTransient(editor, editorIndex, transient); + + return editor; + } + + private doSetTransient(editor: EditorInput, editorIndex: number, transient: boolean): void { + if (transient) { + if (this.transient.has(editor)) { + return; + } + + this.transient.add(editor); + } else { + if (!this.transient.has(editor)) { + return; + } + + this.transient.delete(editor); + } + + // Event + const event: IGroupEditorChangeEvent = { + kind: GroupModelChangeKind.EDITOR_TRANSIENT, + editor, + editorIndex + }; + this._onDidModelChange.fire(event); + } + + isTransient(editorOrIndex: EditorInput | number): boolean { + if (this.transient.size === 0) { + return false; // no transient editor + } + + let editor: EditorInput | undefined; + if (typeof editorOrIndex === 'number') { + editor = this.editors[editorOrIndex]; + } else { + editor = this.findEditor(editorOrIndex)?.[0]; + } + + return !!editor && this.transient.has(editor); + } + private splice(index: number, del: boolean, editor?: EditorInput): void { const editorToDeleteOrReplace = this.editors[index]; @@ -1124,6 +1198,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { dispose(Array.from(this.editorListeners)); this.editorListeners.clear(); + this.transient.clear(); + super.dispose(); } } diff --git a/code/src/vs/workbench/common/editor/editorInput.ts b/code/src/vs/workbench/common/editor/editorInput.ts index 8b80fe942a3..16f23416f2e 100644 --- a/code/src/vs/workbench/common/editor/editorInput.ts +++ b/code/src/vs/workbench/common/editor/editorInput.ts @@ -288,6 +288,20 @@ export abstract class EditorInput extends AbstractEditorInput { return this; } + /** + * Indicates if this editor can be moved to another window. By default + * editors can freely be moved around windows. If an editor cannot be + * moved, a message should be returned to show to the user. + * + * @param targetWindowId the target window to move the editor to. + * @returns `true` if the editor can be moved to the target window, or + * a string with a message to show to the user if the editor cannot be + * moved. + */ + canMove(targetWindowId: number): true | string { + return true; + } + /** * Returns if the other object matches this input. */ diff --git a/code/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/code/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index 7b427fe5ded..390b19874c8 100644 --- a/code/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/code/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -38,6 +38,7 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE get previewEditor(): EditorInput | null { return this.model.previewEditor && this.filter(this.model.previewEditor) ? this.model.previewEditor : null; } isPinned(editorOrIndex: EditorInput | number): boolean { return this.model.isPinned(editorOrIndex); } + isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } diff --git a/code/src/vs/workbench/common/editor/sideBySideEditorInput.ts b/code/src/vs/workbench/common/editor/sideBySideEditorInput.ts index 7228b47ae71..0c6e63d43ce 100644 --- a/code/src/vs/workbench/common/editor/sideBySideEditorInput.ts +++ b/code/src/vs/workbench/common/editor/sideBySideEditorInput.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, ISideBySideEditorInput, IUntypedEditorInput, isResourceSideBySideEditorInput, isDiffEditorInput, isResourceDiffEditorInput, IResourceSideBySideEditorInput, findViewStateForEditor, IMoveResult, isEditorInput, isResourceEditorInput, Verbosity, isResourceMergeEditorInput, isResourceDiffListEditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IRevertOptions, EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, ISideBySideEditorInput, IUntypedEditorInput, isResourceSideBySideEditorInput, isDiffEditorInput, isResourceDiffEditorInput, IResourceSideBySideEditorInput, findViewStateForEditor, IMoveResult, isEditorInput, isResourceEditorInput, Verbosity, isResourceMergeEditorInput, isResourceMultiDiffEditorInput } from 'vs/workbench/common/editor'; import { EditorInput, IUntypedEditorOptions } from 'vs/workbench/common/editor/editorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -210,7 +210,7 @@ export class SideBySideEditorInput extends EditorInput implements ISideBySideEdi return new SideBySideEditorInput(this.preferredName, this.preferredDescription, primarySaveResult, primarySaveResult, this.editorService); } - if (!isResourceDiffEditorInput(primarySaveResult) && !isResourceDiffListEditorInput(primarySaveResult) && !isResourceSideBySideEditorInput(primarySaveResult) && !isResourceMergeEditorInput(primarySaveResult)) { + if (!isResourceDiffEditorInput(primarySaveResult) && !isResourceMultiDiffEditorInput(primarySaveResult) && !isResourceSideBySideEditorInput(primarySaveResult) && !isResourceMergeEditorInput(primarySaveResult)) { return { primary: primarySaveResult, secondary: primarySaveResult, @@ -279,7 +279,7 @@ export class SideBySideEditorInput extends EditorInput implements ISideBySideEdi if ( primaryResourceEditorInput && secondaryResourceEditorInput && !isResourceDiffEditorInput(primaryResourceEditorInput) && !isResourceDiffEditorInput(secondaryResourceEditorInput) && - !isResourceDiffListEditorInput(primaryResourceEditorInput) && !isResourceDiffListEditorInput(secondaryResourceEditorInput) && + !isResourceMultiDiffEditorInput(primaryResourceEditorInput) && !isResourceMultiDiffEditorInput(secondaryResourceEditorInput) && !isResourceSideBySideEditorInput(primaryResourceEditorInput) && !isResourceSideBySideEditorInput(secondaryResourceEditorInput) && !isResourceMergeEditorInput(primaryResourceEditorInput) && !isResourceMergeEditorInput(secondaryResourceEditorInput) ) { diff --git a/code/src/vs/workbench/common/theme.ts b/code/src/vs/workbench/common/theme.ts index febf755414e..8ca7c968f9c 100644 --- a/code/src/vs/workbench/common/theme.ts +++ b/code/src/vs/workbench/common/theme.ts @@ -700,28 +700,42 @@ export const ACTIVITY_BAR_TOP_FOREGROUND = registerColor('activityBarTop.foregro light: '#424242', hcDark: Color.white, hcLight: editorForeground -}, localize('activityBarTop', "Active foreground color of the item in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTop', "Active foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_ACTIVE_BORDER = registerColor('activityBarTop.activeBorder', { dark: ACTIVITY_BAR_TOP_FOREGROUND, light: ACTIVITY_BAR_TOP_FOREGROUND, hcDark: contrastBorder, hcLight: '#B5200D' -}, localize('activityBarTopActiveFocusBorder', "Focus border color for the active item in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTopActiveFocusBorder', "Focus border color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); + +export const ACTIVITY_BAR_TOP_ACTIVE_BACKGROUND = registerColor('activityBarTop.activeBackground', { + dark: null, + light: null, + hcDark: null, + hcLight: null +}, localize('activityBarTopActiveBackground', "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTop.inactiveForeground', { dark: transparent(ACTIVITY_BAR_TOP_FOREGROUND, 0.6), light: transparent(ACTIVITY_BAR_TOP_FOREGROUND, 0.75), hcDark: Color.white, hcLight: editorForeground -}, localize('activityBarTopInActiveForeground', "Inactive foreground color of the item in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTopInActiveForeground', "Inactive foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', { dark: ACTIVITY_BAR_TOP_FOREGROUND, light: ACTIVITY_BAR_TOP_FOREGROUND, hcDark: ACTIVITY_BAR_TOP_FOREGROUND, hcLight: ACTIVITY_BAR_TOP_FOREGROUND -}, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top. The activity allows to switch between views of the side bar.")); +}, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); + +export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', { + dark: null, + light: null, + hcDark: null, + hcLight: null, +}, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); // < --- Profiles --- > @@ -871,6 +885,12 @@ export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeade hcLight: contrastBorder }, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', { + dark: SIDE_BAR_SECTION_HEADER_BORDER, + light: SIDE_BAR_SECTION_HEADER_BORDER, + hcDark: SIDE_BAR_SECTION_HEADER_BORDER, + hcLight: SIDE_BAR_SECTION_HEADER_BORDER +}, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); // < --- Title Bar --- > diff --git a/code/src/vs/workbench/common/views.ts b/code/src/vs/workbench/common/views.ts index 7288219657e..8a2dca5944c 100644 --- a/code/src/vs/workbench/common/views.ts +++ b/code/src/vs/workbench/common/views.ts @@ -284,6 +284,8 @@ export interface IViewDescriptor { readonly containerTitle?: string; + readonly singleViewPaneContainerTitle?: string; + // Applies only to newly created views readonly hideByDefault?: boolean; diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index fb097ebecaa..945637c5dfb 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -13,8 +13,9 @@ import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibi import { HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { SaveAudioCueContribution } from 'vs/workbench/contrib/accessibility/browser/saveAudioCue'; +import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal'; import { CommentsAccessibilityHelpContribution } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; +import { DiffEditorActiveAnnouncementContribution } from 'vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement'; registerAccessibilityConfiguration(); registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed); @@ -29,5 +30,6 @@ workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContri workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(SaveAudioCueContribution.ID, SaveAudioCueContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(SaveAccessibilitySignalContribution.ID, SaveAccessibilitySignalContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(DiffEditorActiveAnnouncementContribution.ID, DiffEditorActiveAnnouncementContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(DynamicSpeechAccessibilityConfiguration.ID, DynamicSpeechAccessibilityConfiguration, WorkbenchPhase.AfterRestored); diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 9a42093991c..51e86960f7c 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { workbenchConfigurationNodeBase, Extensions as WorkbenchExtensions, IConfigurationMigrationRegistry, ConfigurationKeyValuePairs } from 'vs/workbench/common/configuration'; import { AccessibilityAlertSettingId, AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { ISpeechService } from 'vs/workbench/contrib/speech/common/speechService'; +import { ISpeechService, SPEECH_LANGUAGES, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Event } from 'vs/base/common/event'; @@ -22,6 +22,8 @@ export const accessibleViewVerbosityEnabled = new RawContextKey('access export const accessibleViewGoToSymbolSupported = new RawContextKey('accessibleViewGoToSymbolSupported', false, true); export const accessibleViewOnLastLine = new RawContextKey('accessibleViewOnLastLine', false, true); export const accessibleViewCurrentProviderId = new RawContextKey('accessibleViewCurrentProviderId', undefined, undefined); +export const accessibleViewInCodeBlock = new RawContextKey('accessibleViewInCodeBlock', undefined, undefined); +export const accessibleViewContainsCodeBlocks = new RawContextKey('accessibleViewContainsCodeBlocks', undefined, undefined); /** * Miscellaneous settings tagged with accessibility and implemented in the accessibility contrib but @@ -45,6 +47,7 @@ export const enum AccessibilityVerbositySettingId { DiffEditor = 'accessibility.verbosity.diffEditor', Chat = 'accessibility.verbosity.panelChat', InlineChat = 'accessibility.verbosity.inlineChat', + TerminalChat = 'accessibility.verbosity.terminalChat', InlineCompletions = 'accessibility.verbosity.inlineCompletions', KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor', Notebook = 'accessibility.verbosity.notebook', @@ -52,11 +55,13 @@ export const enum AccessibilityVerbositySettingId { Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', - Comments = 'accessibility.verbosity.comments' + Comments = 'accessibility.verbosity.comments', + DiffEditorActive = 'accessibility.verbosity.diffEditorActive' } export const enum AccessibleViewProviderId { Terminal = 'terminal', + TerminalChat = 'terminal-chat', TerminalHelp = 'terminal-help', DiffEditor = 'diffEditor', Chat = 'panelChat', @@ -168,6 +173,10 @@ const configuration: IConfigurationNode = { description: localize('verbosity.comments', 'Provide information about actions that can be taken in the comment widget or in a file which contains comments.'), ...baseVerbosityProperty }, + [AccessibilityVerbositySettingId.DiffEditorActive]: { + description: localize('verbosity.diffEditorActive', 'Indicate when a diff editor becomes the active editor.'), + ...baseVerbosityProperty + }, [AccessibilityAlertSettingId.Save]: { 'markdownDescription': localize('announcement.save', "Indicates when a file is saved. Also see {0}.", '`#audioCues.save#`'), 'enum': ['userGesture', 'always', 'never'], @@ -544,6 +553,30 @@ const configuration: IConfigurationNode = { }, } }, + 'accessibility.signals.voiceRecordingStarted': { + ...defaultNoAnnouncement, + 'description': localize('accessibility.signals.voiceRecordingStarted', "Indicates when the voice recording has started."), + 'properties': { + 'sound': { + 'description': localize('accessibility.signals.voiceRecordingStarted.sound', "Plays a sound when the voice recording has started."), + ...soundFeatureBase, + }, + }, + 'default': { + 'sound': 'on' + } + }, + 'accessibility.signals.voiceRecordingStopped': { + ...defaultNoAnnouncement, + 'description': localize('accessibility.signals.voiceRecordingStopped', "Indicates when the voice recording has stopped."), + 'properties': { + 'sound': { + 'description': localize('accessibility.signals.voiceRecordingStopped.sound', "Plays a sound when the voice recording has stopped."), + ...soundFeatureBase, + default: 'off' + }, + } + }, 'accessibility.signals.clear': { ...signalFeatureBase, 'description': localize('accessibility.signals.clear', "Plays a signal when a feature is cleared (for example, the terminal, Debug Console, or Output channel)."), @@ -664,10 +697,9 @@ export function registerAccessibilityConfiguration() { export const enum AccessibilityVoiceSettingId { SpeechTimeout = 'accessibility.voice.speechTimeout', - SpeechLanguage = 'accessibility.voice.speechLanguage' + SpeechLanguage = SPEECH_LANGUAGE_CONFIG } export const SpeechTimeoutDefault = 1200; -const SpeechLanguageDefault = 'en-US'; export class DynamicSpeechAccessibilityConfiguration extends Disposable implements IWorkbenchContribution { @@ -678,7 +710,7 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen ) { super(); - this._register(Event.runAndSubscribe(speechService.onDidRegisterSpeechProvider, () => this.updateConfiguration())); + this._register(Event.runAndSubscribe(speechService.onDidChangeHasSpeechProvider, () => this.updateConfiguration())); } private updateConfiguration(): void { @@ -703,10 +735,10 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen 'tags': ['accessibility'] }, [AccessibilityVoiceSettingId.SpeechLanguage]: { - 'markdownDescription': localize('voice.speechLanguage', "The language that voice speech recognition should recognize."), + 'markdownDescription': localize('voice.speechLanguage', "The language that voice speech recognition should recognize. Select `auto` to use the configured display language if possible. Note that not all display languages maybe supported by speech recognition"), 'type': 'string', 'enum': languagesSorted, - 'default': SpeechLanguageDefault, + 'default': 'auto', 'tags': ['accessibility'], 'enumDescriptions': languagesSorted.map(key => languages[key].name), 'enumItemLabels': languagesSorted.map(key => languages[key].name) @@ -717,84 +749,10 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen private getLanguages(): { [locale: string]: { name: string } } { return { - ['da-DK']: { - name: localize('speechLanguage.da-DK', "Danish (Denmark)") - }, - ['de-DE']: { - name: localize('speechLanguage.de-DE', "German (Germany)") - }, - ['en-AU']: { - name: localize('speechLanguage.en-AU', "English (Australia)") - }, - ['en-CA']: { - name: localize('speechLanguage.en-CA', "English (Canada)") - }, - ['en-GB']: { - name: localize('speechLanguage.en-GB', "English (United Kingdom)") + ['auto']: { + name: localize('speechLanguage.auto', "Auto (Use Display Language)") }, - ['en-IE']: { - name: localize('speechLanguage.en-IE', "English (Ireland)") - }, - ['en-IN']: { - name: localize('speechLanguage.en-IN', "English (India)") - }, - ['en-NZ']: { - name: localize('speechLanguage.en-NZ', "English (New Zealand)") - }, - [SpeechLanguageDefault]: { - name: localize('speechLanguage.en-US', "English (United States)") - }, - ['es-ES']: { - name: localize('speechLanguage.es-ES', "Spanish (Spain)") - }, - ['es-MX']: { - name: localize('speechLanguage.es-MX', "Spanish (Mexico)") - }, - ['fr-CA']: { - name: localize('speechLanguage.fr-CA', "French (Canada)") - }, - ['fr-FR']: { - name: localize('speechLanguage.fr-FR', "French (France)") - }, - ['hi-IN']: { - name: localize('speechLanguage.hi-IN', "Hindi (India)") - }, - ['it-IT']: { - name: localize('speechLanguage.it-IT', "Italian (Italy)") - }, - ['ja-JP']: { - name: localize('speechLanguage.ja-JP', "Japanese (Japan)") - }, - ['ko-KR']: { - name: localize('speechLanguage.ko-KR', "Korean (South Korea)") - }, - ['nl-NL']: { - name: localize('speechLanguage.nl-NL', "Dutch (Netherlands)") - }, - ['pt-PT']: { - name: localize('speechLanguage.pt-PT', "Portuguese (Portugal)") - }, - ['pt-BR']: { - name: localize('speechLanguage.pt-BR', "Portuguese (Brazil)") - }, - ['ru-RU']: { - name: localize('speechLanguage.ru-RU', "Russian (Russia)") - }, - ['sv-SE']: { - name: localize('speechLanguage.sv-SE', "Swedish (Sweden)") - }, - ['tr-TR']: { - name: localize('speechLanguage.tr-TR', "Turkish (Turkey)") - }, - ['zh-CN']: { - name: localize('speechLanguage.zh-CN', "Chinese (Simplified, China)") - }, - ['zh-HK']: { - name: localize('speechLanguage.zh-HK', "Chinese (Traditional, Hong Kong)") - }, - ['zh-TW']: { - name: localize('speechLanguage.zh-TW', "Chinese (Traditional, Taiwan)") - } + ...SPEECH_LANGUAGES }; } } diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 07fccadd92b..862202d57cf 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -40,8 +40,10 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; 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 { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; +import { IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; const enum DIMENSIONS { @@ -91,6 +93,7 @@ export interface IAccessibleViewService { * @param verbositySettingKey The setting key for the verbosity of the feature */ getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null; + getCodeBlockContext(): ICodeBlockActionContext | undefined; } export const enum AccessibleViewType { @@ -127,6 +130,13 @@ export interface IAccessibleViewOptions { id?: AccessibleViewProviderId; } +interface ICodeBlock { + startLine: number; + endLine: number; + code: string; + languageId?: string; +} + export class AccessibleView extends Disposable { private _editorWidget: CodeEditorWidget; @@ -137,6 +147,9 @@ export class AccessibleView extends Disposable { private _accessibleViewVerbosityEnabled: IContextKey; private _accessibleViewGoToSymbolSupported: IContextKey; private _accessibleViewCurrentProviderId: IContextKey; + private _accessibleViewInCodeBlock: IContextKey; + private _accessibleViewContainsCodeBlocks: IContextKey; + private _codeBlocks?: ICodeBlock[]; get editorWidget() { return this._editorWidget; } private _container: HTMLElement; @@ -159,7 +172,8 @@ export class AccessibleView extends Disposable { @IKeybindingService private readonly _keybindingService: IKeybindingService, @ILayoutService private readonly _layoutService: ILayoutService, @IMenuService private readonly _menuService: IMenuService, - @ICommandService private readonly _commandService: ICommandService + @ICommandService private readonly _commandService: ICommandService, + @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService ) { super(); @@ -169,6 +183,8 @@ export class AccessibleView extends Disposable { this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService); this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService); this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService); + this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService); + this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService); this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService); this._container = document.createElement('div'); @@ -229,6 +245,13 @@ export class AccessibleView extends Disposable { this._register(this._editorWidget.onDidChangeCursorPosition(() => { this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount()); })); + this._register(this._editorWidget.onDidChangeCursorPosition(() => { + const cursorPosition = this._editorWidget.getPosition()?.lineNumber; + if (this._codeBlocks && cursorPosition !== undefined) { + const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined; + this._accessibleViewInCodeBlock.set(inCodeBlock); + } + })); } private _resetContextKeys(): void { @@ -254,6 +277,18 @@ export class AccessibleView extends Disposable { } } + getCodeBlockContext(): ICodeBlockActionContext | undefined { + const position = this._editorWidget.getPosition(); + if (!this._codeBlocks?.length || !position) { + return; + } + const codeBlockIndex = this._codeBlocks?.findIndex(c => c.startLine <= position?.lineNumber && c.endLine >= position?.lineNumber); + const codeBlock = codeBlockIndex !== undefined && codeBlockIndex > -1 ? this._codeBlocks[codeBlockIndex] : undefined; + if (!codeBlock || codeBlockIndex === undefined) { + return; + } + return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined }; + } showLastProvider(id: AccessibleViewProviderId): void { if (!this._lastProvider || this._lastProvider.options.id !== id) { @@ -305,6 +340,9 @@ export class AccessibleView extends Disposable { // only cache a provider with an ID so that it will eventually be cleared. this._lastProvider = provider; } + if (provider.id === AccessibleViewProviderId.Chat) { + this._register(this._codeBlockContextProviderService.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView')); + } } previous(): void { @@ -328,6 +366,35 @@ export class AccessibleView extends Disposable { this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider); } + calculateCodeBlocks(markdown: string): void { + if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { + return; + } + if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') { + // Symbols haven't been provided and we cannot parse this language + return; + } + const lines = markdown.split('\n'); + this._codeBlocks = []; + let inBlock = false; + let startLine = 0; + + let languageId: string | undefined; + lines.forEach((line, i) => { + if (!inBlock && line.startsWith('```')) { + inBlock = true; + startLine = i + 1; + languageId = line.substring(3).trim(); + } else if (inBlock && line.startsWith('```')) { + inBlock = false; + const endLine = i; + const code = lines.slice(startLine, endLine).join('\n'); + this._codeBlocks?.push({ startLine, endLine, code, languageId }); + } + }); + this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0); + } + getSymbols(): IAccessibleViewSymbol[] | undefined { if (!this._currentProvider || !this._currentContent) { return; @@ -430,11 +497,8 @@ export class AccessibleView extends Disposable { } private _render(provider: IAccessibleContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { - if (!showAccessibleViewHelp) { - // don't overwrite the current provider - this._currentProvider = provider; - this._accessibleViewCurrentProviderId.set(provider.id); - } + this._currentProvider = provider; + this._accessibleViewCurrentProviderId.set(provider.id); const value = this._configurationService.getValue(provider.verbositySettingKey); const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : ''; let disableHelpHint = ''; @@ -459,7 +523,9 @@ export class AccessibleView extends Disposable { } const verbose = this._configurationService.getValue(provider.verbositySettingKey); const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; - this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; + const newContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; + this.calculateCodeBlocks(newContent); + this._currentContent = newContent; this._updateContextKeys(provider, true); const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus(); this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { @@ -514,6 +580,7 @@ export class AccessibleView extends Disposable { this._contextViewService.hideContextView(); this._updateContextKeys(provider, false); this._lastProvider = undefined; + this._currentContent = undefined; }; const disposableStore = new DisposableStore(); disposableStore.add(this._editorWidget.onKeyDown((e) => { @@ -618,6 +685,7 @@ export class AccessibleView extends Disposable { const navigationHint = this._getNavigationHint(); const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))."); + const chatHints = this._getChatHints(); let hint = localize('intro', "In the accessible view, you can:\n"); if (navigationHint) { @@ -629,6 +697,37 @@ export class AccessibleView extends Disposable { if (toolbarHint) { hint += ' - ' + toolbarHint + '\n'; } + if (chatHints) { + hint += chatHints; + } + return hint; + } + + private _getChatHints(): string | undefined { + if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { + return; + } + let hint = ''; + const insertAtCursorKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertCodeBlock')?.getAriaLabel(); + const insertIntoNewFileKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertIntoNewFile')?.getAriaLabel(); + const runInTerminalKb = this._keybindingService.lookupKeybinding('workbench.action.chat.runInTerminal')?.getAriaLabel(); + + if (insertAtCursorKb) { + hint += localize('insertAtCursor', " - Insert the code block at the cursor ({0}).\n", insertAtCursorKb); + } else { + hint += localize('insertAtCursorNoKb', " - Insert the code block at the cursor by configuring a keybinding for the Chat: Insert Code Block command.\n"); + } + if (insertIntoNewFileKb) { + hint += localize('insertIntoNewFile', " - Insert the code block into a new file ({0}).\n", insertIntoNewFileKb); + } else { + hint += localize('insertIntoNewFileNoKb', " - Insert the code block into a new file by configuring a keybinding for the Chat: Insert at Cursor command.\n"); + } + if (runInTerminalKb) { + hint += localize('runInTerminal', " - Run the code block in the terminal ({0}).\n", runInTerminalKb); + } else { + hint += localize('runInTerminalNoKb', " - Run the coe block in the terminal by configuring a keybinding for the Chat: Insert into Terminal command.\n"); + } + return hint; } @@ -734,6 +833,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView editorWidget?.revealLine(position.lineNumber); } } + getCodeBlockContext(): ICodeBlockActionContext | undefined { + return this._accessibleView?.getCodeBlockContext(); + } } class AccessibleViewSymbolQuickPick { @@ -775,6 +877,7 @@ export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { markdownToParse?: string; firstListItem?: string; lineNumber?: number; + endLineNumber?: number; } function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean { diff --git a/code/src/vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement.ts b/code/src/vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement.ts new file mode 100644 index 00000000000..ed3d0f75cb0 --- /dev/null +++ b/code/src/vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { localize } from 'vs/nls'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { Event } from 'vs/base/common/event'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; + +export class DiffEditorActiveAnnouncementContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.diffEditorActiveAnnouncement'; + + private _onDidActiveEditorChangeListener?: IDisposable; + + constructor( + @IEditorService private readonly _editorService: IEditorService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IConfigurationService private readonly _configurationService: IConfigurationService + ) { + super(); + this._register(Event.runAndSubscribe(_accessibilityService.onDidChangeScreenReaderOptimized, () => this._updateListener())); + this._register(_configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.DiffEditorActive)) { + this._updateListener(); + } + })); + } + + private _updateListener(): void { + const announcementEnabled = this._configurationService.getValue(AccessibilityVerbositySettingId.DiffEditorActive); + const screenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); + + if (!announcementEnabled || !screenReaderOptimized) { + this._onDidActiveEditorChangeListener?.dispose(); + this._onDidActiveEditorChangeListener = undefined; + return; + } + + if (this._onDidActiveEditorChangeListener) { + return; + } + + this._onDidActiveEditorChangeListener = this._register(this._editorService.onDidActiveEditorChange(() => { + if (isDiffEditor(this._editorService.activeTextEditorControl)) { + this._accessibilityService.alert(localize('openDiffEditorAnnouncement', "Diff editor")); + } + })); + } +} diff --git a/code/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts b/code/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts similarity index 73% rename from code/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts rename to code/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts index 87abfd5cb8d..e4df2fcc74e 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts @@ -9,17 +9,15 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export class SaveAudioCueContribution extends Disposable implements IWorkbenchContribution { +export class SaveAccessibilitySignalContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.saveAudioCues'; + static readonly ID = 'workbench.contrib.saveAccessibilitySignal'; constructor( @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); - this._register(this._workingCopyService.onDidSave((e) => { - this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }); - })); + this._register(this._workingCopyService.onDidSave(e => this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }))); } } diff --git a/code/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts b/code/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts index bff29815a40..44a8f0236d0 100644 --- a/code/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts +++ b/code/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts @@ -6,7 +6,8 @@ import { CachedFunction } from 'vs/base/common/cache'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, autorunDelta, constObservable, debouncedObservable, derived, derivedOpts, observableFromEvent, observableFromPromise, wasEventTriggeredRecently } from 'vs/base/common/observable'; +import { IObservable, IReader, autorun, autorunDelta, derived, derivedOpts, observableFromEvent, observableFromPromise, wasEventTriggeredRecently } from 'vs/base/common/observable'; +import { debouncedObservable2, observableSignalFromEvent } from 'vs/base/common/observableInternal/utils'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; @@ -32,15 +33,35 @@ export class SignalLineFeatureContribution this.instantiationService.createInstance(BreakpointLineFeature), ]; - private readonly isSoundEnabledCache = new CachedFunction>((cue) => observableFromEvent( - this.accessibilitySignalService.onSoundEnabledChanged(cue), - () => this.accessibilitySignalService.isSoundEnabled(cue) + private readonly isEnabledCache = new CachedFunction>((cue) => observableFromEvent( + Event.any( + this.accessibilitySignalService.onSoundEnabledChanged(cue), + this.accessibilitySignalService.onAnnouncementEnabledChanged(cue), + ), + () => this.accessibilitySignalService.isSoundEnabled(cue) || this.accessibilitySignalService.isAnnouncementEnabled(cue) )); - private readonly isAnnouncmentEnabledCahce = new CachedFunction>((cue) => observableFromEvent( - this.accessibilitySignalService.onAnnouncementEnabledChanged(cue), - () => this.accessibilitySignalService.isAnnouncementEnabled(cue) - )); + private readonly _someAccessibilitySignalIsEnabled = derived(this, + (reader) => this.features.some((feature) => + this.isEnabledCache.get(feature.signal).read(reader) + ) + ); + + private readonly _activeEditorObservable = observableFromEvent( + this.editorService.onDidActiveEditorChange, + (_) => { + const activeTextEditorControl = + this.editorService.activeTextEditorControl; + + const editor = isDiffEditor(activeTextEditorControl) + ? activeTextEditorControl.getOriginalEditor() + : isCodeEditor(activeTextEditorControl) + ? activeTextEditorControl + : undefined; + + return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined; + } + ); constructor( @IEditorService private readonly editorService: IEditorService, @@ -50,38 +71,16 @@ export class SignalLineFeatureContribution ) { super(); - const someAccessibilitySignalIsEnabled = derived( - (reader) => /** @description someAccessibilitySignalFeatureIsEnabled */ this.features.some((feature) => - this.isSoundEnabledCache.get(feature.signal).read(reader) || this.isAnnouncmentEnabledCahce.get(feature.signal).read(reader) - ) - ); - - const activeEditorObservable = observableFromEvent( - this.editorService.onDidActiveEditorChange, - (_) => { - const activeTextEditorControl = - this.editorService.activeTextEditorControl; - - const editor = isDiffEditor(activeTextEditorControl) - ? activeTextEditorControl.getOriginalEditor() - : isCodeEditor(activeTextEditorControl) - ? activeTextEditorControl - : undefined; - - return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined; - } - ); this._register( autorun(reader => { /** @description updateSignalsEnabled */ this.store.clear(); - if (!someAccessibilitySignalIsEnabled.read(reader)) { + if (!this._someAccessibilitySignalIsEnabled.read(reader)) { return; } - - const activeEditor = activeEditorObservable.read(reader); + const activeEditor = this._activeEditorObservable.read(reader); if (activeEditor) { this.registerAccessibilitySignalsForEditor(activeEditor.editor, activeEditor.model, this.store); } @@ -109,26 +108,26 @@ export class SignalLineFeatureContribution return editor.getPosition(); } ); - const debouncedPosition = debouncedObservable(curPosition, this._configurationService.getValue('accessibility.signals.debouncePositionChanges') ? 300 : 0, store); + const debouncedPosition = debouncedObservable2(curPosition, this._configurationService.getValue('accessibility.signals.debouncePositionChanges') ? 300 : 0); const isTyping = wasEventTriggeredRecently( - editorModel.onDidChangeContent.bind(editorModel), + e => editorModel.onDidChangeContent(e), 1000, store ); const featureStates = this.features.map((feature) => { - const lineFeatureState = feature.getObservableState(editor, editorModel); + const lineFeatureState = feature.createSource(editor, editorModel); const isFeaturePresent = derivedOpts( { debugName: `isPresentInLine:${feature.signal.name}` }, (reader) => { - if (!this.isSoundEnabledCache.get(feature.signal).read(reader) && !this.isAnnouncmentEnabledCahce.get(feature.signal).read(reader)) { + if (!this.isEnabledCache.get(feature.signal).read(reader)) { return false; } const position = debouncedPosition.read(reader); if (!position) { return false; } - return lineFeatureState.read(reader).isPresent(position); + return lineFeatureState.isPresent(position, reader); } ); return derivedOpts( @@ -161,23 +160,23 @@ export class SignalLineFeatureContribution (!lastValue?.featureStates?.get(feature) || newValue.lineNumber !== lastValue.lineNumber) ); - this.accessibilitySignalService.playAccessibilitySignals(newFeatures.map(f => f.signal)); + this.accessibilitySignalService.playSignals(newFeatures.map(f => f.signal)); }) ); } } interface LineFeature { - signal: AccessibilitySignal; - debounceWhileTyping?: boolean; - getObservableState( + readonly signal: AccessibilitySignal; + readonly debounceWhileTyping?: boolean; + createSource( editor: ICodeEditor, model: ITextModel - ): IObservable; + ): LineFeatureSource; } -interface LineFeatureState { - isPresent(position: Position): boolean; +interface LineFeatureSource { + isPresent(position: Position, reader: IReader): boolean; } class MarkerLineFeature implements LineFeature { @@ -190,53 +189,46 @@ class MarkerLineFeature implements LineFeature { ) { } - getObservableState(editor: ICodeEditor, model: ITextModel): IObservable { - return observableFromEvent( - Event.filter(this.markerService.onMarkerChanged, (changedUris) => - changedUris.some((u) => u.toString() === model.uri.toString()) - ), - () => /** @description this.markerService.onMarkerChanged */({ - isPresent: (position) => { - const lineChanged = position.lineNumber !== this._previousLine; - this._previousLine = position.lineNumber; - const hasMarker = this.markerService - .read({ resource: model.uri }) - .some( - (m) => { - const onLine = m.severity === this.severity && m.startLineNumber <= position.lineNumber && position.lineNumber <= m.endLineNumber; - return lineChanged ? onLine : onLine && (position.lineNumber <= m.endLineNumber && m.startColumn <= position.column && m.endColumn >= position.column); - }); - return hasMarker; - }, - }) - ); + createSource(editor: ICodeEditor, model: ITextModel): LineFeatureSource { + const obs = observableSignalFromEvent('onMarkerChanged', this.markerService.onMarkerChanged); + return { + isPresent: (position, reader) => { + obs.read(reader); + const lineChanged = position.lineNumber !== this._previousLine; + this._previousLine = position.lineNumber; + const hasMarker = this.markerService + .read({ resource: model.uri }) + .some( + (m) => { + const onLine = m.severity === this.severity && m.startLineNumber <= position.lineNumber && position.lineNumber <= m.endLineNumber; + return lineChanged ? onLine : onLine && (position.lineNumber <= m.endLineNumber && m.startColumn <= position.column && m.endColumn >= position.column); + }); + return hasMarker; + }, + }; } } class FoldedAreaLineFeature implements LineFeature { public readonly signal = AccessibilitySignal.foldedArea; - getObservableState(editor: ICodeEditor, model: ITextModel): IObservable { + createSource(editor: ICodeEditor, _model: ITextModel): LineFeatureSource { const foldingController = FoldingController.get(editor); if (!foldingController) { - return constObservable({ - isPresent: () => false, - }); + return { isPresent: () => false, }; } - - const foldingModel = observableFromPromise( - foldingController.getFoldingModel() ?? Promise.resolve(undefined) - ); - return foldingModel.map((v) => ({ - isPresent: (position) => { - const regionAtLine = v.value?.getRegionAtLine(position.lineNumber); + const foldingModel = observableFromPromise(foldingController.getFoldingModel() ?? Promise.resolve(undefined)); + return { + isPresent: (position, reader) => { + const m = foldingModel.read(reader); + const regionAtLine = m.value?.getRegionAtLine(position.lineNumber); const hasFolding = !regionAtLine ? false : regionAtLine.isCollapsed && regionAtLine.startLineNumber === position.lineNumber; return hasFolding; }, - })); + }; } } @@ -245,18 +237,17 @@ class BreakpointLineFeature implements LineFeature { constructor(@IDebugService private readonly debugService: IDebugService) { } - getObservableState(editor: ICodeEditor, model: ITextModel): IObservable { - return observableFromEvent( - this.debugService.getModel().onDidChangeBreakpoints, - () => /** @description debugService.getModel().onDidChangeBreakpoints */({ - isPresent: (position) => { - const breakpoints = this.debugService - .getModel() - .getBreakpoints({ uri: model.uri, lineNumber: position.lineNumber }); - const hasBreakpoints = breakpoints.length > 0; - return hasBreakpoints; - }, - }) - ); + createSource(editor: ICodeEditor, model: ITextModel): LineFeatureSource { + const signal = observableSignalFromEvent('onDidChangeBreakpoints', this.debugService.getModel().onDidChangeBreakpoints); + return { + isPresent: (position, reader) => { + signal.read(reader); + const breakpoints = this.debugService + .getModel() + .getBreakpoints({ uri: model.uri, lineNumber: position.lineNumber }); + const hasBreakpoints = breakpoints.length > 0; + return hasBreakpoints; + }, + }; } } diff --git a/code/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts b/code/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts index 329cabe4bf3..ec79963a27f 100644 --- a/code/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts +++ b/code/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts @@ -76,7 +76,7 @@ export class ShowSignalSoundHelp extends Action2 { qp.onDidChangeActive(() => { accessibilitySignalService.playSound(qp.activeItems[0].signal.sound.getSound(true), true); }); - qp.placeholder = localize('audioCues.help.placeholder', 'Select a sound to play and configure'); + qp.placeholder = localize('sounds.help.placeholder', 'Select a sound to play and configure'); qp.canSelectMany = true; await qp.show(); } diff --git a/code/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/code/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index ecc7d689e27..5acfa3942f1 100644 --- a/code/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/code/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -27,24 +26,29 @@ import { IRequestService, asText } from 'vs/platform/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { isWeb } from 'vs/base/common/platform'; +import { isInternalTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; -const configurationKey = 'workbench.accounts.experimental.showEntitlements'; +const accountsBadgeConfigKey = 'workbench.accounts.experimental.showEntitlements'; +const chatWelcomeViewConfigKey = 'workbench.chat.experimental.showWelcomeView'; type EntitlementEnablementClassification = { - enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if the account entitlement is enabled' }; + enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if the entitlement is enabled' }; owner: 'bhavyaus'; - comment: 'Reporting when the account entitlement is shown'; + comment: 'Reporting when the entitlement is shown'; }; type EntitlementActionClassification = { command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The command being executed by the entitlement action' }; owner: 'bhavyaus'; - comment: 'Reporting the account entitlement action'; + comment: 'Reporting the entitlement action'; }; -class AccountsEntitlement extends Disposable implements IWorkbenchContribution { +class EntitlementsContribution extends Disposable implements IWorkbenchContribution { + private isInitialized = false; - private contextKey = new RawContextKey(configurationKey, true).bindTo(this.contextService); + private showAccountsBadgeContextKey = new RawContextKey(accountsBadgeConfigKey, false).bindTo(this.contextService); + private showChatWelcomeViewContextKey = new RawContextKey(chatWelcomeViewConfigKey, false).bindTo(this.contextService); + private accountsMenuBadgeDisposable = this._register(new MutableDisposable()); constructor( @IContextKeyService readonly contextService: IContextKeyService, @@ -57,32 +61,17 @@ class AccountsEntitlement extends Disposable implements IWorkbenchContribution { @IActivityService readonly activityService: IActivityService, @IExtensionService readonly extensionService: IExtensionService, @IConfigurationService readonly configurationService: IConfigurationService, - @IContextKeyService readonly contextKeyService: IContextKeyService, - @IRequestService readonly requestService: IRequestService, - ) { + @IRequestService readonly requestService: IRequestService) { super(); if (!this.productService.gitHubEntitlement || isWeb) { return; } - // if previously shown, do not show again. - const showEntitlements = this.storageService.getBoolean(configurationKey, StorageScope.APPLICATION, true); - if (!showEntitlements) { - return; - } - - const setting = this.configurationService.inspect(configurationKey); - if (!setting.value) { - return; - } - - this.extensionManagementService.getInstalled().then(exts => { + this.extensionManagementService.getInstalled().then(async exts => { const installed = exts.find(value => ExtensionIdentifier.equals(value.identifier.id, this.productService.gitHubEntitlement!.extensionId)); if (installed) { - this.storageService.store(configurationKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); - this.contextKey.set(false); - return; + this.disableEntitlements(); } else { this.registerListeners(); } @@ -90,35 +79,38 @@ class AccountsEntitlement extends Disposable implements IWorkbenchContribution { } private registerListeners() { + this._register(this.extensionService.onDidChangeExtensions(async (result) => { for (const ext of result.added) { if (ExtensionIdentifier.equals(this.productService.gitHubEntitlement!.extensionId, ext.identifier)) { - this.storageService.store(configurationKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); - this.contextKey.set(false); + this.disableEntitlements(); return; } } })); this._register(this.authenticationService.onDidChangeSessions(async (e) => { - if (e.providerId === this.productService.gitHubEntitlement!.providerId && e.event.added?.length && !this.isInitialized) { - this.onSessionChange(e.event.added[0]); + if (e.providerId === this.productService.gitHubEntitlement!.providerId && e.event.added?.length) { + await this.enableEntitlements(e.event.added[0]); } else if (e.providerId === this.productService.gitHubEntitlement!.providerId && e.event.removed?.length) { - this.contextKey.set(false); + this.showAccountsBadgeContextKey.set(false); + this.showChatWelcomeViewContextKey.set(false); + this.accountsMenuBadgeDisposable.clear(); } })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async e => { - if (e.id === this.productService.gitHubEntitlement!.providerId && !this.isInitialized) { - const session = await this.authenticationService.getSessions(e.id); - this.onSessionChange(session[0]); + if (e.id === this.productService.gitHubEntitlement!.providerId) { + await this.enableEntitlements((await this.authenticationService.getSessions(e.id))[0]); } })); } - private async onSessionChange(session: AuthenticationSession) { + private async getEntitlementsInfo(session: AuthenticationSession): Promise<[enabled: boolean, org: string | undefined]> { - this.isInitialized = true; + if (this.isInitialized) { + return [false, '']; + } const context = await this.requestService.request({ type: 'GET', @@ -129,11 +121,11 @@ class AccountsEntitlement extends Disposable implements IWorkbenchContribution { }, CancellationToken.None); if (context.res.statusCode && context.res.statusCode !== 200) { - return; + return [false, '']; } const result = await asText(context); if (!result) { - return; + return [false, '']; } let parsedResult: any; @@ -142,25 +134,54 @@ class AccountsEntitlement extends Disposable implements IWorkbenchContribution { } catch (err) { //ignore - return; + return [false, '']; } if (!(this.productService.gitHubEntitlement!.enablementKey in parsedResult) || !parsedResult[this.productService.gitHubEntitlement!.enablementKey]) { - return; + this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>('entitlements.enabled', { enabled: false }); + return [false, '']; } + this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>('entitlements.enabled', { enabled: true }); + this.isInitialized = true; + const orgs = parsedResult['organization_login_list'] as any[]; + return [true, orgs ? orgs[orgs.length - 1] : undefined]; + } - this.contextKey.set(true); - this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(configurationKey, { enabled: true }); + private async enableEntitlements(session: AuthenticationSession) { + const isInternal = isInternalTelemetry(this.productService, this.configurationService) ?? true; + const showAccountsBadge = this.configurationService.inspect(accountsBadgeConfigKey).value ?? false; + const showWelcomeView = this.configurationService.inspect(chatWelcomeViewConfigKey).value ?? false; - const orgs = parsedResult['organization_login_list'] as any[]; - const menuTitle = orgs ? this.productService.gitHubEntitlement!.command.title.replace('{{org}}', orgs[orgs.length - 1]) : this.productService.gitHubEntitlement!.command.titleWithoutPlaceHolder; + const [enabled, org] = await this.getEntitlementsInfo(session); + if (enabled) { + if (isInternal && showWelcomeView) { + this.showChatWelcomeViewContextKey.set(true); + this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(chatWelcomeViewConfigKey, { enabled: true }); + } + if (showAccountsBadge) { + this.createAccountsBadge(org); + this.showAccountsBadgeContextKey.set(showAccountsBadge); + this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(accountsBadgeConfigKey, { enabled: true }); + } + } + } - const badge = new NumberBadge(1, () => menuTitle); - const accountsMenuBadgeDisposable = this._register(new MutableDisposable()); - accountsMenuBadgeDisposable.value = this.activityService.showAccountsActivity({ badge, }); + private disableEntitlements() { + this.storageService.store(accountsBadgeConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); + this.storageService.store(chatWelcomeViewConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); + this.showAccountsBadgeContextKey.set(false); + this.showChatWelcomeViewContextKey.set(false); + this.accountsMenuBadgeDisposable.clear(); + } + + private async createAccountsBadge(org: string | undefined) { + const menuTitle = org ? this.productService.gitHubEntitlement!.command.title.replace('{{org}}', org) : this.productService.gitHubEntitlement!.command.titleWithoutPlaceHolder; - registerAction2(class extends Action2 { + const badge = new NumberBadge(1, () => menuTitle); + this.accountsMenuBadgeDisposable.value = this.activityService.showAccountsActivity({ badge, }); + + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.entitlementAction', @@ -169,7 +190,7 @@ class AccountsEntitlement extends Disposable implements IWorkbenchContribution { menu: { id: MenuId.AccountsContext, group: '5_AccountsEntitlements', - when: ContextKeyExpr.equals(configurationKey, true), + when: ContextKeyExpr.equals(accountsBadgeConfigKey, true), } }); } @@ -201,19 +222,14 @@ class AccountsEntitlement extends Disposable implements IWorkbenchContribution { }); } - accountsMenuBadgeDisposable.clear(); - const contextKey = new RawContextKey(configurationKey, true).bindTo(contextKeyService); + const contextKey = new RawContextKey(accountsBadgeConfigKey, true).bindTo(contextKeyService); contextKey.set(false); - storageService.store(configurationKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); + storageService.store(accountsBadgeConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); } - }); + })); } } -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(AccountsEntitlement, LifecyclePhase.Eventually); - - const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ ...applicationConfigurationNodeBase, @@ -227,3 +243,18 @@ configurationRegistry.registerConfiguration({ } } }); + +configurationRegistry.registerConfiguration({ + ...applicationConfigurationNodeBase, + properties: { + 'workbench.chat.experimental.showWelcomeView': { + scope: ConfigurationScope.MACHINE, + type: 'boolean', + default: false, + tags: ['experimental'], + description: localize('workbench.chat.showWelcomeView', "When enabled, the chat panel welcome view will be shown.") + } + } +}); + +registerWorkbenchContribution2('workbench.contrib.entitlements', EntitlementsContribution, WorkbenchPhase.BlockRestore); diff --git a/code/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts b/code/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts new file mode 100644 index 00000000000..6559535304c --- /dev/null +++ b/code/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { fromNow } from 'vs/base/common/date'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export class ManageTrustedExtensionsForAccountAction extends Action2 { + constructor() { + super({ + id: '_manageTrustedExtensionsForAccount', + title: localize('manageTrustedExtensionsForAccount', "Manage Trusted Extensions For Account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const productService = accessor.get(IProductService); + const extensionService = accessor.get(IExtensionService); + const dialogService = accessor.get(IDialogService); + const quickInputService = accessor.get(IQuickInputService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allowedExtensions = authenticationAccessService.readAllowedExtensions(providerId, accountLabel); + const trustedExtensionAuthAccess = productService.trustedExtensionAuthAccess; + const trustedExtensionIds = + // Case 1: trustedExtensionAuthAccess is an array + Array.isArray(trustedExtensionAuthAccess) + ? trustedExtensionAuthAccess + // Case 2: trustedExtensionAuthAccess is an object + : typeof trustedExtensionAuthAccess === 'object' + ? trustedExtensionAuthAccess[providerId] ?? [] + : []; + for (const extensionId of trustedExtensionIds) { + const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId); + if (!allowedExtension) { + // Add the extension to the allowedExtensions list + const extension = await extensionService.getExtension(extensionId); + if (extension) { + allowedExtensions.push({ + id: extensionId, + name: extension.displayName || extension.name, + allowed: true, + trusted: true + }); + } + } else { + // Update the extension to be allowed + allowedExtension.allowed = true; + allowedExtension.trusted = true; + } + } + + if (!allowedExtensions.length) { + dialogService.info(localize('noTrustedExtensions', "This account has not been used by any extensions.")); + return; + } + + interface TrustedExtensionsQuickPickItem extends IQuickPickItem { + extension: AllowedExtension; + lastUsed?: number; + } + + const disposableStore = new DisposableStore(); + const quickPick = disposableStore.add(quickInputService.createQuickPick()); + quickPick.canSelectMany = true; + quickPick.customButton = true; + quickPick.customLabel = localize('manageTrustedExtensions.cancel', 'Cancel'); + const usages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + const trustedExtensions = []; + const otherExtensions = []; + for (const extension of allowedExtensions) { + const usage = usages.find(usage => extension.id === usage.extensionId); + extension.lastUsed = usage?.lastUsed; + if (extension.trusted) { + trustedExtensions.push(extension); + } else { + otherExtensions.push(extension); + } + } + + const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0); + const toQuickPickItem = function (extension: AllowedExtension) { + const lastUsed = extension.lastUsed; + const description = lastUsed + ? localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true)) + : localize('notUsed', "Has not used this account"); + let tooltip: string | undefined; + if (extension.trusted) { + tooltip = localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); + } + return { + label: extension.name, + extension, + description, + tooltip + }; + }; + const items: Array = [ + ...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem), + { type: 'separator', label: localize('trustedExtensions', "Trusted by Microsoft") }, + ...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem) + ]; + + quickPick.items = items; + quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed)); + quickPick.title = localize('manageTrustedExtensions', "Manage Trusted Extensions"); + quickPick.placeholder = localize('manageExtensions', "Choose which extensions can access this account"); + + disposableStore.add(quickPick.onDidAccept(() => { + const updatedAllowedList = quickPick.items + .filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator') + .map(i => i.extension); + authenticationAccessService.updateAllowedExtensions(providerId, accountLabel, updatedAllowedList); + quickPick.hide(); + })); + + disposableStore.add(quickPick.onDidChangeSelection((changed) => { + const trustedItems = new Set(); + quickPick.items.forEach(item => { + const trustItem = item as TrustedExtensionsQuickPickItem; + if (trustItem.extension) { + if (trustItem.extension.trusted) { + trustedItems.add(trustItem); + } else { + trustItem.extension.allowed = false; + } + } + }); + changed.forEach((item) => { + item.extension.allowed = true; + trustedItems.delete(item); + }); + + // reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection + if (trustedItems.size) { + quickPick.selectedItems = [...changed, ...trustedItems]; + } + })); + + disposableStore.add(quickPick.onDidHide(() => { + disposableStore.dispose(); + })); + + disposableStore.add(quickPick.onDidCustom(() => { + quickPick.hide(); + })); + + quickPick.show(); + } + +} diff --git a/code/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts b/code/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts new file mode 100644 index 00000000000..87afd379e24 --- /dev/null +++ b/code/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; + +export class SignOutOfAccountAction extends Action2 { + constructor() { + super({ + id: '_signOutOfAccount', + title: localize('signOutOfAccount', "Sign out of account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const authenticationService = accessor.get(IAuthenticationService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + const dialogService = accessor.get(IDialogService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allSessions = await authenticationService.getSessions(providerId); + const sessions = allSessions.filter(s => s.account.label === accountLabel); + + const accountUsages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + + const { confirmed } = await dialogService.confirm({ + type: Severity.Info, + message: accountUsages.length + ? localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountLabel, accountUsages.map(usage => usage.extensionName).join('\n')) + : localize('signOutMessageSimple', "Sign out of '{0}'?", accountLabel), + primaryButton: localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") + }); + + if (confirmed) { + const removeSessionPromises = sessions.map(session => authenticationService.removeSession(providerId, session.id)); + await Promise.all(removeSessionPromises); + authenticationUsageService.removeAccountUsage(providerId, accountLabel); + authenticationAccessService.removeAllowedExtensions(providerId, accountLabel); + } + } +} diff --git a/code/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/code/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts new file mode 100644 index 00000000000..36d4e4e1352 --- /dev/null +++ b/code/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { SignOutOfAccountAction } from 'vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction'; +import { AuthenticationProviderInformation, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ManageTrustedExtensionsForAccountAction } from './actions/manageTrustedExtensionsForAccountAction'; + +const codeExchangeProxyCommand = CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { + const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); + return environmentService.options?.codeExchangeProxyEndpoints; +}); + +const authenticationDefinitionSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + description: localize('authentication.id', 'The id of the authentication provider.') + }, + label: { + type: 'string', + description: localize('authentication.label', 'The human readable name of the authentication provider.'), + } + } +}; + +const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'authentication', + jsonSchema: { + description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), + type: 'array', + items: authenticationDefinitionSchema + }, + activationEventsGenerator: (authenticationProviders, result) => { + for (const authenticationProvider of authenticationProviders) { + if (authenticationProvider.id) { + result.push(`onAuthenticationRequest:${authenticationProvider.id}`); + } + } + } +}); + +class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + + readonly type = 'table'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.authentication; + } + + render(manifest: IExtensionManifest): IRenderedData { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('authenticationlabel', "Label"), + localize('authenticationid', "ID"), + ]; + + const rows: IRowData[][] = authentication + .sort((a, b) => a.label.localeCompare(b.label)) + .map(auth => { + return [ + auth.label, + auth.id, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +const extensionFeature = Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'authentication', + label: localize('authentication', "Authentication"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(AuthenticationDataRenderer), +}); + +export class AuthenticationContribution extends Disposable implements IWorkbenchContribution { + static ID = 'workbench.contrib.authentication'; + + private _placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('authentication.Placeholder', "No accounts requested yet..."), + precondition: ContextKeyExpr.false() + }, + }); + + constructor( + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService + ) { + super(); + this._register(codeExchangeProxyCommand); + this._register(extensionFeature); + + this._registerHandlers(); + this._registerAuthenticationExtentionPointHandler(); + this._registerEnvContributedAuthenticationProviders(); + this._registerActions(); + } + + private _registerAuthenticationExtentionPointHandler(): void { + authenticationExtPoint.setHandler((extensions, { added, removed }) => { + added.forEach(point => { + for (const provider of point.value) { + if (isFalsyOrWhitespace(provider.id)) { + point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + continue; + } + + if (isFalsyOrWhitespace(provider.label)) { + point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + continue; + } + + if (!this._authenticationService.declaredProviders.some(p => p.id === provider.id)) { + this._authenticationService.registerDeclaredAuthenticationProvider(provider); + } else { + point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + } + }); + + const removedExtPoints = removed.flatMap(r => r.value); + removedExtPoints.forEach(point => { + const provider = this._authenticationService.declaredProviders.find(provider => provider.id === point.id); + if (provider) { + this._authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + } + }); + }); + } + + private _registerEnvContributedAuthenticationProviders(): void { + if (!this._environmentService.options?.authenticationProviders?.length) { + return; + } + for (const provider of this._environmentService.options.authenticationProviders) { + this._authenticationService.registerAuthenticationProvider(provider.id, provider); + } + } + + private _registerHandlers(): void { + this._register(this._authenticationService.onDidRegisterAuthenticationProvider(_e => { + this._placeholderMenuItem?.dispose(); + this._placeholderMenuItem = undefined; + })); + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(_e => { + if (!this._authenticationService.getProviderIds().length) { + this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('loading', "Loading..."), + precondition: ContextKeyExpr.false() + } + }); + } + })); + } + + private _registerActions(): void { + this._register(registerAction2(SignOutOfAccountAction)); + this._register(registerAction2(ManageTrustedExtensionsForAccountAction)); + } +} + +registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored); diff --git a/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index ba85dee93b2..6f4e6cce624 100644 --- a/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -100,6 +100,12 @@ export class BulkEditPane extends ViewPane { this._ctxHasCategories = BulkEditPane.ctxHasCategories.bindTo(contextKeyService); this._ctxGroupByFile = BulkEditPane.ctxGroupByFile.bindTo(contextKeyService); this._ctxHasCheckedChanges = BulkEditPane.ctxHasCheckedChanges.bindTo(contextKeyService); + // telemetry + type BulkEditPaneOpened = { + owner: 'aiday-mar'; + comment: 'Report when the bulk edit pane has been opened'; + }; + this.telemetryService.publicLog2<{}, BulkEditPaneOpened>('views.bulkEditPane'); } override dispose(): void { diff --git a/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index c5fd28d6a8e..e45a95008c3 100644 --- a/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/code/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -580,7 +580,7 @@ class TextEditElementTemplate { this._icon = document.createElement('div'); container.appendChild(this._icon); - this._label = new HighlightedLabel(container); + this._label = this._disposables.add(new HighlightedLabel(container)); } dispose(): void { diff --git a/code/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts b/code/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts index 76497826d74..22c507ca0b3 100644 --- a/code/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts +++ b/code/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { mockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -35,7 +35,7 @@ suite('BulkCellEdits', function () { const edits = [ new ResourceNotebookCellEdit(inputUri, { index: 0, count: 1, editType: CellEditType.Replace, cells: [] }) ]; - const bce = new BulkCellEdits(new UndoRedoGroup(), new UndoRedoSource(), progress, new CancellationTokenSource().token, edits, editorService, notebookService as any); + const bce = new BulkCellEdits(new UndoRedoGroup(), new UndoRedoSource(), progress, CancellationToken.None, edits, editorService, notebookService as any); await bce.apply(); const resolveArgs = notebookService.resolve.args[0]; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 9fd67d69b88..92b430ff162 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -12,7 +12,7 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleDiffViewerNext } from 'vs/editor/browser/widget/diffEditor/diffEditor.contribution'; +import { AccessibleDiffViewerNext } from 'vs/editor/browser/widget/diffEditor/commands'; export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string { const keybindingService = accessor.get(IKeybindingService); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 27f5c9f07f8..2c53083e14f 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -24,8 +24,9 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidgetService, IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatCopyKind, IChatService, IDocumentContext } from 'vs/workbench/contrib/chat/common/chatService'; @@ -87,7 +88,7 @@ export function registerChatCodeBlockActions() { icon: Codicon.copy, menu: { id: MenuId.ChatCodeBlock, - group: 'navigation', + group: 'navigation' } }); } @@ -147,21 +148,24 @@ export function registerChatCodeBlockActions() { // Report copy to extensions const chatService = accessor.get(IChatService); - chatService.notifyUserAction({ - providerId: context.element.providerId, - agentId: context.element.agent?.id, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'copy', - codeBlockIndex: context.codeBlockIndex, - copyKind: ChatCopyKind.Action, - copiedText, - copiedCharacters: copiedText.length, - totalCharacters, - } - }); + const element = context.element as IChatResponseViewModel | undefined; + if (element) { + chatService.notifyUserAction({ + providerId: element.providerId, + agentId: element.agent?.id, + sessionId: element.sessionId, + requestId: element.requestId, + result: element.result, + action: { + kind: 'copy', + codeBlockIndex: context.codeBlockIndex, + copyKind: ChatCopyKind.Action, + copiedText, + copiedCharacters: copiedText.length, + totalCharacters, + } + }); + } // Copy full cell if no selection, otherwise fall back on normal editor implementation if (noSelection) { @@ -184,10 +188,10 @@ export function registerChatCodeBlockActions() { menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - when: CONTEXT_IN_CHAT_SESSION, + when: CONTEXT_IN_CHAT_SESSION }, keybinding: { - when: ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), + when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), primary: KeyMod.CtrlCmd | KeyCode.Enter, mac: { primary: KeyMod.WinCtrl | KeyCode.Enter }, weight: KeybindingWeight.WorkbenchContrib @@ -351,7 +355,7 @@ export function registerChatCodeBlockActions() { menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - isHiddenByDefault: true, + isHiddenByDefault: true } }); } @@ -431,7 +435,7 @@ export function registerChatCodeBlockActions() { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter }, weight: KeybindingWeight.EditorContrib, - when: CONTEXT_IN_CHAT_SESSION, + when: ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, accessibleViewInCodeBlock), }] }); } @@ -557,20 +561,23 @@ export function registerChatCodeBlockActions() { }); } -function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): IChatCodeBlockActionContext | undefined { +function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { const chatWidgetService = accessor.get(IChatWidgetService); + const chatCodeBlockContextProviderService = accessor.get(IChatCodeBlockContextProviderService); const model = editor.getModel(); if (!model) { return; } const widget = chatWidgetService.lastFocusedWidget; - if (!widget) { - return; - } - - const codeBlockInfo = widget.getCodeBlockInfoForEditor(model.uri); + const codeBlockInfo = widget?.getCodeBlockInfoForEditor(model.uri); if (!codeBlockInfo) { + for (const provider of chatCodeBlockContextProviderService.providers) { + const context = provider.getCodeBlockContext(editor); + if (context) { + return context; + } + } return; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 0d34b49aff8..33047b3b5b5 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,62 +3,62 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh } from 'vs/base/common/platform'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; -import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions'; import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; +import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; +import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; -import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; +import { ILanguageModelsService, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; -import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; -import { LanguageModelsService, ILanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; -import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; -import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; -import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; -import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -92,7 +92,12 @@ 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.implicitContext': { + type: 'boolean', + description: nls.localize('chat.experimental.implicitContext', "Controls whether a checkbox is shown to allow the user to determine which implicit context is included with a chat participant's prompt."), + default: false + }, } }); @@ -246,7 +251,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { executeImmediately: true }, async (prompt, progress) => { const defaultAgent = chatAgentService.getDefaultAgent(); - const agents = chatAgentService.getAgents(); + const agents = chatAgentService.getRegisteredAgents(); // Report prefix if (defaultAgent?.metadata.helpTextPrefix) { @@ -266,8 +271,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`; - const commands = await a.provideSlashCommands(undefined, [], CancellationToken.None); - const commandText = commands.map(c => { + const commandText = a.slashCommands.map(c => { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${c.description}`; @@ -333,3 +337,4 @@ registerSingleton(IChatSlashCommandService, ChatSlashCommandService, Instantiati registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); +registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/code/src/vs/workbench/contrib/chat/browser/chat.ts b/code/src/vs/workbench/contrib/chat/browser/chat.ts index daa5ac41eb1..f4efe913e16 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chat.ts @@ -4,18 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; +import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; export const IChatWidgetService = createDecorator('chatWidgetService'); -export const IQuickChatService = createDecorator('quickChatService'); -export const IChatAccessibilityService = createDecorator('chatAccessibilityService'); export interface IChatWidgetService { @@ -36,6 +37,7 @@ export interface IChatWidgetService { getWidgetBySessionId(sessionId: string): IChatWidget | undefined; } +export const IQuickChatService = createDecorator('quickChatService'); export interface IQuickChatService { readonly _serviceBrand: undefined; readonly onDidClose: Event; @@ -63,6 +65,7 @@ export interface IQuickChatOpenOptions { selection?: Selection; } +export const IChatAccessibilityService = createDecorator('chatAccessibilityService'); export interface IChatAccessibilityService { readonly _serviceBrand: undefined; acceptRequest(): number; @@ -134,3 +137,17 @@ export interface IChatWidget { export interface IChatViewPane { clear(): void; } + + +export interface ICodeBlockActionContextProvider { + getCodeBlockContext(editor?: ICodeEditor): ICodeBlockActionContext | undefined; +} + +export const IChatCodeBlockContextProviderService = createDecorator('chatCodeBlockContextProviderService'); +export interface IChatCodeBlockContextProviderService { + readonly _serviceBrand: undefined; + readonly providers: ICodeBlockActionContextProvider[]; + registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable; +} + +export const GeneratingPhrase = localize('generating', "Generating"); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts new file mode 100644 index 00000000000..933a8940869 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { marked } from 'vs/base/common/marked/marked'; +import { localize } from 'vs/nls'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { isRequestVM, isResponseVM, isWelcomeVM, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatAccessibilityProvider implements IListAccessibilityProvider { + + constructor( + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + ) { + + } + getWidgetRole(): AriaRole { + return 'list'; + } + + getRole(element: ChatTreeItem): AriaRole | undefined { + return 'listitem'; + } + + getWidgetAriaLabel(): string { + return localize('chat', "Chat"); + } + + getAriaLabel(element: ChatTreeItem): string { + if (isRequestVM(element)) { + return element.messageText; + } + + if (isResponseVM(element)) { + return this._getLabelWithCodeBlockCount(element); + } + + if (isWelcomeVM(element)) { + return element.content.map(c => 'value' in c ? c.value : c.map(followup => followup.message).join('\n')).join('\n'); + } + + return ''; + } + + private _getLabelWithCodeBlockCount(element: IChatResponseViewModel): string { + const accessibleViewHint = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.Chat); + let label: string = ''; + const fileTreeCount = element.response.value.filter((v) => !('value' in v))?.length ?? 0; + let fileTreeCountHint = ''; + switch (fileTreeCount) { + case 0: + break; + case 1: + fileTreeCountHint = localize('singleFileTreeHint', "1 file tree"); + break; + default: + fileTreeCountHint = localize('multiFileTreeHint', "{0} file trees", fileTreeCount); + break; + } + const codeBlockCount = marked.lexer(element.response.asString()).filter(token => token.type === 'code')?.length ?? 0; + switch (codeBlockCount) { + case 0: + label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.asString()); + break; + case 1: + label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.asString()); + break; + default: + label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.asString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.asString()); + break; + } + return label; + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index b6c66797247..73a0ebd76a9 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -15,7 +15,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi declare readonly _serviceBrand: undefined; - private _pendingCueMap: DisposableMap = this._register(new DisposableMap()); + private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); private _requestId: number = 0; @@ -25,11 +25,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi acceptRequest(): number { this._requestId++; this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); - this._pendingCueMap.set(this._requestId, this._instantiationService.createInstance(AudioCueScheduler)); + this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilitySignalScheduler)); return this._requestId; } acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { - this._pendingCueMap.deleteAndDispose(requestId); + this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.asString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); @@ -46,19 +46,19 @@ const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; /** * Schedules an audio cue to play when a chat response is pending for too long. */ -class AudioCueScheduler extends Disposable { +class AccessibilitySignalScheduler extends Disposable { private _scheduler: RunOnceScheduler; - private _audioCueLoop: IDisposable | undefined; + private _signalLoop: IDisposable | undefined; constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { super(); this._scheduler = new RunOnceScheduler(() => { - this._audioCueLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); + this._signalLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS); this._scheduler.schedule(); } override dispose(): void { super.dispose(); - this._audioCueLoop?.dispose(); + this._signalLoop?.dispose(); this._scheduler.cancel(); this._scheduler.dispose(); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/code/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 8498292395a..2b358ca4519 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -7,8 +7,10 @@ import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { localize, localize2 } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -18,10 +20,10 @@ import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chat import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { IChatContributionService, IChatProviderContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; - const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'interactiveSession', jsonSchema: { @@ -59,20 +61,151 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi }, }); +const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'chatParticipants', + jsonSchema: { + description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('chatParticipantName', "Unique name for this Chat Participant."), + type: 'string' + }, + description: { + description: localize('chatParticipantDescription', "A description of this Chat Participant, shown in the UI."), + type: 'string' + }, + isDefault: { + markdownDescription: localize('chatParticipantIsDefaultDescription', "**Only** allowed for extensions that have the `defaultChatParticipant` proposal."), + type: 'boolean', + }, + defaultImplicitVariables: { + markdownDescription: '**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default', + type: 'array', + items: { + type: 'string' + } + }, + commands: { + markdownDescription: localize('chatCommandsDescription', "Commands available for this Chat Participant, which the user can invoke with a `/`."), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."), + type: 'string' + }, + description: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + when: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + sampleRequest: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + isSticky: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'boolean' + }, + defaultImplicitVariables: { + markdownDescription: localize('defaultImplicitVariables', "**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default"), + type: 'array', + items: { + type: 'string' + } + }, + } + } + }, + locations: { + markdownDescription: localize('chatLocationsDescription', "Locations in which this Chat Participant is available."), + type: 'array', + default: ['panel'], + items: { + type: 'string', + enum: ['panel', 'terminal', 'notebook'] + } + + } + } + } + }, + activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { + for (const contrib of contributions) { + result.push(`onChatParticipant:${contrib.name}`); + } + }, +}); + export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; + private readonly disposables = new DisposableStore(); + private _welcomeViewDescriptor?: IViewDescriptor; private _viewContainer: ViewContainer; private _registrationDisposables = new Map(); constructor( - @IChatContributionService readonly _chatContributionService: IChatContributionService + @IChatContributionService readonly _chatContributionService: IChatContributionService, + @IProductService readonly productService: IProductService, + @IContextKeyService readonly contextService: IContextKeyService, + @ILogService readonly logService: ILogService, ) { this._viewContainer = this.registerViewContainer(); + this.registerListeners(); this.handleAndRegisterChatExtensions(); } + private registerListeners() { + this.contextService.onDidChangeContext(e => { + + if (!this.productService.chatWelcomeView) { + return; + } + + const showWelcomeViewConfigKey = 'workbench.chat.experimental.showWelcomeView'; + const keys = new Set([showWelcomeViewConfigKey]); + if (e.affectsSome(keys)) { + const contextKeyExpr = ContextKeyExpr.equals(showWelcomeViewConfigKey, true); + const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + if (this.contextService.contextMatchesRules(contextKeyExpr)) { + const viewId = this._chatContributionService.getViewIdForProvider(this.productService.chatWelcomeView.welcomeViewId); + + this._welcomeViewDescriptor = { + id: viewId, + name: { original: this.productService.chatWelcomeView.welcomeViewTitle, value: this.productService.chatWelcomeView.welcomeViewTitle }, + containerIcon: this._viewContainer.icon, + ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ providerId: this.productService.chatWelcomeView.welcomeViewId }]), + canToggleVisibility: false, + canMoveView: true, + order: 100 + }; + viewsRegistry.registerViews([this._welcomeViewDescriptor], this._viewContainer); + + viewsRegistry.registerViewWelcomeContent(viewId, { + content: this.productService.chatWelcomeView.welcomeViewContent, + }); + } else if (this._welcomeViewDescriptor) { + viewsRegistry.deregisterViews([this._welcomeViewDescriptor], this._viewContainer); + } + } + }, null, this.disposables); + } + private handleAndRegisterChatExtensions(): void { chatExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { @@ -96,6 +229,30 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } } }); + + chatParticipantExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const providerDescriptor of extension.value) { + if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); + continue; + } + + if (providerDescriptor.defaultImplicitVariables && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); + continue; + } + + this._chatContributionService.registerChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + + for (const extension of delta.removed) { + for (const providerDescriptor of extension.value) { + this._chatContributionService.deregisterChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + }); } private registerViewContainer(): ViewContainer { @@ -123,6 +280,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { id: viewId, containerIcon: this._viewContainer.icon, containerTitle: this._viewContainer.title.value, + singleViewPaneContainerTitle: this._viewContainer.title.value, name: { value: providerDescriptor.label, original: providerDescriptor.label }, canToggleVisibility: false, canMoveView: true, @@ -156,10 +314,15 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +function getParticipantKey(participant: IChatParticipantContribution): string { + return `${participant.extensionId.value}_${participant.name}`; +} + export class ChatContributionService implements IChatContributionService { declare _serviceBrand: undefined; private _registeredProviders = new Map(); + private _registeredParticipants = new Map(); constructor( ) { } @@ -176,7 +339,19 @@ export class ChatContributionService implements IChatContributionService { this._registeredProviders.delete(providerId); } + public registerChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.set(getParticipantKey(participant), participant); + } + + public deregisterChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.delete(getParticipantKey(participant)); + } + public get registeredProviders(): IChatProviderContribution[] { return Array.from(this._registeredProviders.values()); } + + public get registeredParticipants(): IChatParticipantContribution[] { + return Array.from(this._registeredParticipants.values()); + } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 4651778b165..d184cb7525c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -20,6 +20,7 @@ import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInp import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { IChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IChatEditorOptions extends IEditorOptions { target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData }; @@ -37,13 +38,14 @@ export class ChatEditor extends EditorPane { private _viewState: IChatViewState | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { - super(ChatEditorInput.EditorID, telemetryService, themeService, storageService); + super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } public async clear() { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index e149d81cadb..ca64eb2a930 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import * as aria from 'vs/base/browser/ui/aria/aria'; +import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; import { IAction } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; import { HistoryNavigator } from 'vs/base/common/history'; @@ -15,6 +16,8 @@ import { isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { IDimension } from 'vs/editor/common/core/dimension'; +import { IPosition } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { HoverController } from 'vs/editor/contrib/hover/browser/hover'; @@ -28,9 +31,12 @@ import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/b import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; +import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { asCssVariableWithDefault, checkboxBorder, inputBackground } from 'vs/platform/theme/common/colorRegistry'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; +import { ChatSubmitEditorAction, ChatSubmitSecondaryAgentEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatExecuteActionContext, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; @@ -41,8 +47,6 @@ import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; -import { ChatSubmitEditorAction, ChatSubmitSecondaryAgentEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IPosition } from 'vs/editor/common/core/position'; const $ = dom.$; @@ -73,6 +77,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private followupsContainer!: HTMLElement; private followupsDisposables = this._register(new DisposableStore()); + private implicitContextContainer!: HTMLElement; + private implicitContextLabel!: HTMLElement; + private implicitContextCheckbox!: Checkbox; + private implicitContextSettingEnabled = false; + get implicitContextEnabled() { + return this.implicitContextCheckbox.checked; + } + + private _inputPartHeight: number = 0; + get inputPartHeight() { + return this._inputPartHeight; + } + private _inputEditor!: CodeEditorWidget; private _inputEditorElement!: HTMLElement; @@ -115,10 +132,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.history = new HistoryNavigator([], 5); this._register(this.historyService.onDidClearHistory(() => this.history.clear())); + + this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { this.inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); } + + if (e.affectsConfiguration('chat.experimental.implicitContext')) { + this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); + } })); } @@ -227,6 +250,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')); + this.implicitContextContainer = dom.append(this.container, $('.chat-implicit-context')); + this.initImplicitContext(this.implicitContextContainer); const inputAndSideToolbar = dom.append(this.container, $('.interactive-input-and-side-toolbar')); const inputContainer = dom.append(inputAndSideToolbar, $('.interactive-input-and-execute-toolbar')); @@ -350,6 +375,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + private initImplicitContext(container: HTMLElement) { + this.implicitContextCheckbox = new Checkbox('#selection', true, { ...defaultCheckboxStyles, checkboxBorder: asCssVariableWithDefault(checkboxBorder, inputBackground) }); + container.append(this.implicitContextCheckbox.domNode); + this.implicitContextLabel = dom.append(container, $('span.chat-implicit-context-label')); + this.implicitContextLabel.textContent = '#selection'; + } + + setImplicitContextKinds(kinds: string[]) { + dom.setVisibility(this.implicitContextSettingEnabled && kinds.length > 0, this.implicitContextContainer); + this.implicitContextLabel.textContent = localize('use', "Use") + ' ' + kinds.map(k => `#${k}`).join(', '); + } + async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { if (!this.options.renderFollowups) { return; @@ -362,37 +399,43 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - layout(height: number, width: number): number { + layout(height: number, width: number) { this.cachedDimensions = new dom.Dimension(width, height); return this._layout(height, width); } - private _layout(height: number, width: number, allowRecurse = true): number { + private previousInputEditorDimension: IDimension | undefined; + private _layout(height: number, width: number, allowRecurse = true): void { const followupsHeight = this.followupsContainer.offsetHeight; const inputPartBorder = 1; const inputPartHorizontalPadding = 40; const inputPartVerticalPadding = 24; const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), height - followupsHeight - inputPartHorizontalPadding - inputPartBorder, INPUT_EDITOR_MAX_HEIGHT); + const implicitContextHeight = this.implicitContextContainer.offsetHeight; const inputEditorBorder = 2; - const inputPartHeight = followupsHeight + inputEditorHeight + inputPartVerticalPadding + inputPartBorder + inputEditorBorder; + this._inputPartHeight = followupsHeight + inputEditorHeight + inputPartVerticalPadding + inputPartBorder + inputEditorBorder + implicitContextHeight; const editorBorder = 2; - const editorPadding = 8; + const editorPadding = 12; const executeToolbarWidth = this.cachedToolbarWidth = this.toolbar.getItemsWidth(); const sideToolbarWidth = this.options.renderStyle === 'compact' ? 20 : 0; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); - this._inputEditor.layout({ width: width - inputPartHorizontalPadding - editorBorder - editorPadding - executeToolbarWidth - sideToolbarWidth, height: inputEditorHeight }); + const newDimension = { width: width - inputPartHorizontalPadding - editorBorder - editorPadding - executeToolbarWidth - sideToolbarWidth, height: inputEditorHeight }; + if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { + // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler + // to be invoked, and we have a lot of these on this editor. Only doing a layout this when the editor size has actually changed makes it much easier to follow. + this._inputEditor.layout(newDimension); + this.previousInputEditorDimension = newDimension; + } 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 return this._layout(height, width, false); } - - return inputPartHeight; } saveState(): void { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index c0f1827014a..1637f0846ce 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -5,11 +5,10 @@ import * as dom from 'vs/base/browser/dom'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { AriaRole, alert } from 'vs/base/browser/ui/aria/aria'; +import { alert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; @@ -21,19 +20,17 @@ import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { marked } from 'vs/base/common/marked/marked'; import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; import { basename } from 'vs/base/common/path'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { Range } from 'vs/editor/common/core/range'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -41,7 +38,6 @@ import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; 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 { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { FileKind, FileType } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -52,13 +48,11 @@ import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; -import { ChatMarkdownDecorationsRenderer, annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatMarkdownDecorationsRenderer, IMarkdownVulnerability, annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatCodeBlockContentProvider, ICodeBlockData, ICodeBlockPart, LocalFileCodeBlockPart, SimpleCodeBlockPart, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatCodeBlockContentProvider, CodeBlockPart, ICodeBlockData, ICodeBlockPart, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -68,6 +62,7 @@ import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownR import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; const $ = dom.$; @@ -102,6 +97,7 @@ export interface IChatListItemRendererOptions { readonly renderStyle?: 'default' | 'compact'; readonly noHeader?: boolean; readonly noPadding?: boolean; + readonly editableCodeBlock?: boolean; } export class ChatListItemRenderer extends Disposable implements ITreeRenderer { @@ -133,47 +129,30 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer => { - if (input.resource.scheme !== Schemas.vscodeChatCodeBlock) { - return null; - } - const block = this._editorPool.find(input.resource); - if (!block) { - return null; - } - if (input.options?.selection) { - block.editor.setSelection({ - startLineNumber: input.options.selection.startLineNumber, - startColumn: input.options.selection.startColumn, - endLineNumber: input.options.selection.startLineNumber ?? input.options.selection.endLineNumber, - endColumn: input.options.selection.startColumn ?? input.options.selection.endColumn - }); - } - return block.editor; - })); - this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences') ?? true; this._register(configService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('chat.experimental.usedReferences')) { @@ -186,6 +165,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIconUri).toString(true); + avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); } else { const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(defaultIcon)); + const icon = element.avatarIcon ?? defaultIcon; + const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } - if (isResponseVM(element) && element.agent && !element.agent.metadata.isDefault) { + if (isResponseVM(element) && element.agent && !element.agent.isDefault) { dom.show(templateData.agentAvatarContainer); const icon = this.getAgentIcon(element.agent.metadata); if (icon instanceof URI) { @@ -754,7 +738,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - let data: ICodeBlockData; + const index = codeBlockIndex++; + let textModel: Promise>; + let range: Range | undefined; + let vulns: readonly IMarkdownVulnerability[] | undefined; if (equalsIgnoreCase(languageId, localFileLanguageId)) { try { const parsedBody = parseLocalFileData(text); - data = { type: 'localFile', uri: parsedBody.uri, range: parsedBody.range && Range.lift(parsedBody.range), codeBlockIndex: codeBlockIndex++, element, hideToolbar: false, parentContextKeyService: templateData.contextKeyService }; + range = parsedBody.range && Range.lift(parsedBody.range); + textModel = this.textModelService.createModelReference(parsedBody.uri); } catch (e) { - console.error(e); return $('div'); } } else { - const vulns = extractVulnerabilitiesFromText(text); - const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - data = { type: 'code', languageId, text: vulns.newText, codeBlockIndex: codeBlockIndex++, element, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns: vulns.vulnerabilities }; + if (!isRequestVM(element) && !isResponseVM(element)) { + console.error('Trying to render code block in welcome', element.id, index); + return $('div'); + } + + const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; + const blockModel = this.codeBlockModelCollection.get(sessionId, element, index); + if (!blockModel) { + console.error('Trying to render code block without model', element.id, index); + return $('div'); + } + + textModel = blockModel; + const extractedVulns = extractVulnerabilitiesFromText(text); + vulns = extractedVulns.vulnerabilities; + textModel.then(ref => ref.object.textEditorModel.setValue(extractedVulns.newText)); } - const ref = this.renderCodeBlock(data); + const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; + const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns }); // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) @@ -899,15 +900,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.codeBlocksByEditorUri.delete(ref.object.uri))); + if (ref.object.uri) { + const uri = ref.object.uri; + this.codeBlocksByEditorUri.set(uri, info); + disposables.add(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); + } } orderedDisposablesList.push(ref); return ref.object.element; @@ -933,9 +937,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - const ref = this._editorPool.get(data); + const ref = this._editorPool.get(); const editorInfo = ref.object; - editorInfo.render(data, this._currentLayoutWidth); + editorInfo.render(data, this._currentLayoutWidth, this.rendererOptions.editableCodeBlock); return ref; } @@ -996,113 +1000,34 @@ export class ChatListDelegate implements IListVirtualDelegate { } } -export class ChatAccessibilityProvider implements IListAccessibilityProvider { - - constructor( - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService - ) { - - } - getWidgetRole(): AriaRole { - return 'list'; - } - - getRole(element: ChatTreeItem): AriaRole | undefined { - return 'listitem'; - } - - getWidgetAriaLabel(): string { - return localize('chat', "Chat"); - } - - getAriaLabel(element: ChatTreeItem): string { - if (isRequestVM(element)) { - return element.messageText; - } - - if (isResponseVM(element)) { - return this._getLabelWithCodeBlockCount(element); - } - - if (isWelcomeVM(element)) { - return element.content.map(c => 'value' in c ? c.value : c.map(followup => followup.message).join('\n')).join('\n'); - } - - return ''; - } - - private _getLabelWithCodeBlockCount(element: IChatResponseViewModel): string { - const accessibleViewHint = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.Chat); - let label: string = ''; - const fileTreeCount = element.response.value.filter((v) => !('value' in v))?.length ?? 0; - let fileTreeCountHint = ''; - switch (fileTreeCount) { - case 0: - break; - case 1: - fileTreeCountHint = localize('singleFileTreeHint', "1 file tree"); - break; - default: - fileTreeCountHint = localize('multiFileTreeHint', "{0} file trees", fileTreeCount); - break; - } - const codeBlockCount = marked.lexer(element.response.asString()).filter(token => token.type === 'code')?.length ?? 0; - switch (codeBlockCount) { - case 0: - label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.asString()); - break; - case 1: - label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.asString()); - break; - default: - label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.asString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.asString()); - break; - } - return label; - } -} - interface IDisposableReference extends IDisposable { object: T; isStale: () => boolean; } -class EditorPool extends Disposable { +export class EditorPool extends Disposable { - private readonly _simpleEditorPool: ResourcePool; - private readonly _localFileEditorPool: ResourcePool; + private readonly _pool: ResourcePool; - public *inUse(): Iterable { - yield* this._simpleEditorPool.inUse; - yield* this._localFileEditorPool.inUse; + public inUse(): Iterable { + return this._pool.inUse; } constructor( - private readonly options: ChatEditorOptions, + options: ChatEditorOptions, delegate: IChatRendererDelegate, overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); - this._simpleEditorPool = this._register(new ResourcePool(() => { - return this.instantiationService.createInstance(SimpleCodeBlockPart, this.options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); - })); - this._localFileEditorPool = this._register(new ResourcePool(() => { - return this.instantiationService.createInstance(LocalFileCodeBlockPart, this.options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); })); } - get(data: ICodeBlockData): IDisposableReference { - return this.getFromPool(data.type === 'localFile' ? this._localFileEditorPool : this._simpleEditorPool); - } - - find(resource: URI): SimpleCodeBlockPart | undefined { - return Array.from(this._simpleEditorPool.inUse).find(part => part.uri?.toString() === resource.toString()); - } - - private getFromPool(pool: ResourcePool): IDisposableReference { - const codeBlock = pool.get(); + get(): IDisposableReference { + const codeBlock = this._pool.get(); let stale = false; return { object: codeBlock, @@ -1110,7 +1035,7 @@ class EditorPool extends Disposable { dispose: () => { codeBlock.reset(); stale = true; - pool.release(codeBlock); + this._pool.release(codeBlock); } }; } @@ -1137,7 +1062,7 @@ class TreePool extends Disposable { const resourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility }); const container = $('.interactive-response-progress-tree'); - createFileIconThemableTreeContainerScope(container, this.themeService); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); const tree = >this.instantiationService.createInstance( WorkbenchCompressibleAsyncDataTree, @@ -1197,7 +1122,7 @@ class ContentReferencesListPool extends Disposable { const resourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility }); const container = $('.chat-used-context-list'); - createFileIconThemableTreeContainerScope(container, this.themeService); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); const list = >this.instantiationService.createInstance( WorkbenchList, diff --git a/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 147b267245c..971e0434e96 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -40,9 +40,7 @@ export class ChatVariablesService implements IChatVariablesService { const data = this._resolver.get(part.variableName.toLowerCase()); if (data) { jobs.push(data.resolver(prompt.text, part.variableArg, model, progress, token).then(values => { - if (values?.length) { - resolvedVariables[i] = { name: part.variableName, range: part.range, values }; - } + resolvedVariables[i] = { name: part.variableName, range: part.range, values: values ?? [] }; }).catch(onUnexpectedExternalError)); } } else if (part instanceof ChatRequestDynamicVariablePart) { @@ -62,6 +60,15 @@ export class ChatVariablesService implements IChatVariablesService { }; } + async resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + const data = this._resolver.get(variableName.toLowerCase()); + if (!data) { + return Promise.resolve([]); + } + + return (await data.resolver(promptText, undefined, model, progress, token)) ?? []; + } + hasVariable(name: string): boolean { return this._resolver.has(name.toLowerCase()); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts index ab61b6bde4f..e0f24df80d4 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -5,37 +5,42 @@ import * as dom from 'vs/base/browser/dom'; import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree'; -import { disposableTimeout } from 'vs/base/common/async'; +import { disposableTimeout, timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/chat'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAccessibilityProvider } from 'vs/workbench/contrib/chat/browser/chatAccessibilityProvider'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { ChatAccessibilityProvider, ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IParsedChatRequest, chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; const $ = dom.$; @@ -98,6 +103,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private tree!: WorkbenchObjectTree; private renderer!: ChatListItemRenderer; + private readonly _codeBlockModelCollection: CodeBlockModelCollection; private inputPart!: ChatInputPart; private editorOptions!: ChatEditorOptions; @@ -149,6 +155,7 @@ export class ChatWidget extends Disposable implements IChatWidget { readonly viewContext: IChatWidgetViewContext, private readonly viewOptions: IChatWidgetViewOptions, private readonly styles: IChatWidgetStyles, + @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IChatService private readonly chatService: IChatService, @@ -158,13 +165,51 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, - @IThemeService private readonly _themeService: IThemeService + @IThemeService private readonly _themeService: IThemeService, ) { super(); CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); this.requestInProgress = CONTEXT_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); this._register((chatWidgetService as ChatWidgetService).register(this)); + + this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); + + this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { + if (input.resource.scheme !== Schemas.vscodeChatCodeBlock) { + return null; + } + + const responseId = input.resource.path.split('/').at(1); + if (!responseId) { + return null; + } + + const item = this.viewModel?.getItems().find(item => item.id === responseId); + if (!item) { + return null; + } + + this.reveal(item); + + await timeout(0); // wait for list to actually render + + for (const editor of this.renderer.editorsInUse() ?? []) { + if (editor.uri?.toString() === input.resource.toString()) { + const inner = editor.editor; + if (input.options?.selection) { + inner.setSelection({ + startLineNumber: input.options.selection.startLineNumber, + startColumn: input.options.selection.startColumn, + endLineNumber: input.options.selection.startLineNumber ?? input.options.selection.endLineNumber, + endColumn: input.options.selection.startColumn ?? input.options.selection.endColumn + }); + } + return inner; + } + } + return null; + })); } get supportsFileReferences(): boolean { @@ -232,6 +277,9 @@ export class ChatWidget extends Disposable implements IChatWidget { } moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void { + if (!isResponseVM(item)) { + return; + } const items = this.viewModel?.getItems(); if (!items) { return; @@ -340,6 +388,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.editorOptions, options, rendererDelegate, + this._codeBlockModelCollection, overflowWidgetsContainer, )); this._register(this.renderer.onDidClickFollowup(item => { @@ -377,7 +426,7 @@ export class ChatWidget extends Disposable implements IChatWidget { listFocusAndSelectionForeground: this.styles.listForeground, } }); - this.tree.onContextMenu(e => this.onContextMenu(e)); + this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onDidChangeContentHeight(() => { this.onDidChangeTreeContentHeight(); @@ -475,8 +524,12 @@ export class ChatWidget extends Disposable implements IChatWidget { }); })); this._register(this.inputPart.onDidChangeHeight(() => this.bodyDimension && this.layout(this.bodyDimension.height, this.bodyDimension.width))); - this._register(this.inputEditor.onDidChangeModelContent(() => this.parsedChatRequest = undefined)); - this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); + this._register(this.inputEditor.onDidChangeModelContent(() => this.updateImplicitContextKinds())); + this._register(this.chatAgentService.onDidChangeAgents(() => { + if (this.viewModel) { + this.updateImplicitContextKinds(); + } + })); } private onDidStyleChange(): void { @@ -485,13 +538,29 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.style.setProperty('--vscode-chat-list-background', this._themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); } + private updateImplicitContextKinds() { + this.parsedChatRequest = undefined; + const agentAndSubcommand = extractAgentAndCommand(this.parsedInput); + const currentAgent = agentAndSubcommand.agentPart?.agent ?? this.chatAgentService.getDefaultAgent(); + const implicitVariables = agentAndSubcommand.commandPart ? + agentAndSubcommand.commandPart.command.defaultImplicitVariables : + currentAgent?.defaultImplicitVariables; + this.inputPart.setImplicitContextKinds(implicitVariables ?? []); + + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + } + setModel(model: IChatModel, viewState: IChatViewState): void { if (!this.container) { throw new Error('Call render() before setModel()'); } + this._codeBlockModelCollection.clear(); + this.container.setAttribute('data-session-id', model.sessionId); - this.viewModel = this.instantiationService.createInstance(ChatViewModel, model); + this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection); this.viewModelDisposables.add(this.viewModel.onDidChange(e => { this.requestInProgress.set(this.viewModel!.requestInProgress); this.onDidChangeItems(); @@ -519,6 +588,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(); revealLastElement(this.tree); } + + this.updateImplicitContextKinds(); } getFocus(): ChatTreeItem | undefined { @@ -584,7 +655,7 @@ export class ChatWidget extends Disposable implements IChatWidget { 'query' in opts ? opts.query : `${opts.prefix} ${editorValue}`; const isUserQuery = !opts || 'prefix' in opts; - const result = await this.chatService.sendRequest(this.viewModel.sessionId, input); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled); if (result) { const inputState = this.collectInputState(); @@ -634,7 +705,8 @@ export class ChatWidget extends Disposable implements IChatWidget { width = Math.min(width, 850); this.bodyDimension = new dom.Dimension(width, height); - const inputPartHeight = this.inputPart.layout(height, width); + this.inputPart.layout(height, width); + const inputPartHeight = this.inputPart.inputPartHeight; const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; const listHeight = height - inputPartHeight; @@ -680,7 +752,8 @@ export class ChatWidget extends Disposable implements IChatWidget { const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight); const width = this.bodyDimension?.width ?? this.container.offsetWidth; - const inputPartHeight = this.inputPart.layout(possibleMaxHeight, width); + this.inputPart.layout(possibleMaxHeight, width); + const inputPartHeight = this.inputPart.inputPartHeight; const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight); this.layout(newHeight + inputPartHeight, width); }); @@ -723,7 +796,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } const width = this.bodyDimension?.width ?? this.container.offsetWidth; - const inputHeight = this.inputPart.layout(this._dynamicMessageLayoutData.maxHeight, width); + this.inputPart.layout(this._dynamicMessageLayoutData.maxHeight, width); + const inputHeight = this.inputPart.inputPartHeight; const totalMessages = this.viewModel.getItems(); // grab the last N messages @@ -757,6 +831,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.saveState(); return { inputValue: this.getInput(), inputState: this.collectInputState() }; } + + } export class ChatWidgetService implements IChatWidgetService { diff --git a/code/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts b/code/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts new file mode 100644 index 00000000000..8c790c54040 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ICodeBlockActionContextProvider, IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; + +export class ChatCodeBlockContextProviderService implements IChatCodeBlockContextProviderService { + declare _serviceBrand: undefined; + private readonly _providers = new Map(); + + get providers(): ICodeBlockActionContextProvider[] { + return [...this._providers.values()]; + } + registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { + this._providers.set(id, provider); + return toDisposable(() => this._providers.delete(id)); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 8669cadcce7..25c28adb9fc 100644 --- a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -9,18 +9,15 @@ import * as dom from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EDITOR_FONT_DEFAULTS, EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -50,27 +47,19 @@ import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/ const $ = dom.$; -interface ICodeBlockDataCommon { - codeBlockIndex: number; - element: unknown; - parentContextKeyService?: IContextKeyService; - hideToolbar?: boolean; -} +export interface ICodeBlockData { + readonly codeBlockIndex: number; + readonly element: unknown; -export interface ISimpleCodeBlockData extends ICodeBlockDataCommon { - type: 'code'; - text: string; - languageId: string; - vulns?: IMarkdownVulnerability[]; -} + readonly textModel: Promise>; + readonly languageId: string; -export interface ILocalFileCodeBlockData extends ICodeBlockDataCommon { - type: 'localFile'; - uri: URI; - range?: Range; -} + readonly vulns?: readonly IMarkdownVulnerability[]; + readonly range?: Range; -export type ICodeBlockData = ISimpleCodeBlockData | ILocalFileCodeBlockData; + readonly parentContextKeyService?: IContextKeyService; + readonly hideToolbar?: boolean; +} /** * Special markdown code block language id used to render a local file. @@ -112,25 +101,26 @@ export function parseLocalFileData(text: string) { export interface ICodeBlockActionContext { code: string; - languageId: string; + languageId?: string; codeBlockIndex: number; element: unknown; } -export interface ICodeBlockPart { +export interface ICodeBlockPart { + readonly editor: CodeEditorWidget; readonly onDidChangeContentHeight: Event; readonly element: HTMLElement; - readonly uri: URI; + readonly uri: URI | undefined; layout(width: number): void; - render(data: Data, width: number): Promise; + render(data: ICodeBlockData, width: number, editable?: boolean): Promise; focus(): void; reset(): unknown; dispose(): void; } const defaultCodeblockPadding = 10; -abstract class BaseCodeBlockPart extends Disposable implements ICodeBlockPart { +export class CodeBlockPart extends Disposable implements ICodeBlockPart { protected readonly _onDidChangeContentHeight = this._register(new Emitter()); public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; @@ -138,9 +128,12 @@ abstract class BaseCodeBlockPart extends Disposable protected readonly toolbar: MenuWorkbenchToolBar; private readonly contextKeyService: IContextKeyService; - abstract readonly uri: URI; public readonly element: HTMLElement; + private readonly vulnsButton: Button; + private readonly vulnsListElement: HTMLElement; + + private currentCodeBlockData: ICodeBlockData | undefined; private currentScrollWidth = 0; constructor( @@ -152,7 +145,7 @@ abstract class BaseCodeBlockPart extends Disposable @IContextKeyService contextKeyService: IContextKeyService, @IModelService protected readonly modelService: IModelService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { super(); this.element = $('.interactive-result-code-block'); @@ -173,6 +166,13 @@ abstract class BaseCodeBlockPart extends Disposable scrollbar: { alwaysConsumeMouseWheel: false }, + definitionLinkOpensInPeek: false, + gotoLocation: { + multiple: 'goto', + multipleDeclarations: 'goto', + multipleDefinitions: 'goto', + multipleImplementations: 'goto', + }, ariaLabel: localize('chat.codeBlockHelp', 'Code block'), overflowWidgetsDomNode, ...this.getEditorOptionsFromConfig(), @@ -187,6 +187,31 @@ abstract class BaseCodeBlockPart extends Disposable } })); + const vulnsContainer = dom.append(this.element, $('.interactive-result-vulns')); + const vulnsHeaderElement = dom.append(vulnsContainer, $('.interactive-result-vulns-header', undefined)); + this.vulnsButton = this._register(new Button(vulnsHeaderElement, { + buttonBackground: undefined, + buttonBorder: undefined, + buttonForeground: undefined, + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined, + supportIcons: true + })); + + this.vulnsListElement = dom.append(vulnsContainer, $('ul.interactive-result-vulns-list')); + + this._register(this.vulnsButton.onDidClick(() => { + const element = this.currentCodeBlockData!.element as IChatResponseViewModel; + element.vulnerabilitiesListExpanded = !element.vulnerabilitiesListExpanded; + this.vulnsButton.label = this.getVulnerabilitiesLabel(); + this.element.classList.toggle('chat-vulnerabilities-collapsed', !element.vulnerabilitiesListExpanded); + this._onDidChangeContentHeight.fire(); + // this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); + })); + this._register(this.toolbar.onDidChangeDropdownVisibility(e => { toolbarElement.classList.toggle('force-visibility', e); })); @@ -229,7 +254,27 @@ abstract class BaseCodeBlockPart extends Disposable } } - protected abstract createEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): CodeEditorWidget; + get uri(): URI | undefined { + return this.editor.getModel()?.uri; + } + + private createEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): CodeEditorWidget { + return this._register(instantiationService.createInstance(CodeEditorWidget, parent, options, { + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + + WordHighlighterContribution.ID, + ViewportSemanticTokensContribution.ID, + BracketMatchingController.ID, + SmartSelectController.ID, + HoverController.ID, + GotoDefinitionAtPositionEditorContribution.ID, + ]) + })); + } focus(): void { this.editor.focus(); @@ -277,17 +322,22 @@ abstract class BaseCodeBlockPart extends Disposable this.updatePaddingForLayout(); } - protected getContentHeight() { + private getContentHeight() { + if (this.currentCodeBlockData?.range) { + const lineCount = this.currentCodeBlockData.range.endLineNumber - this.currentCodeBlockData.range.startLineNumber + 1; + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + return lineCount * lineHeight; + } return this.editor.getContentHeight(); } - async render(data: Data, width: number) { + async render(data: ICodeBlockData, width: number, editable: boolean) { if (data.parentContextKeyService) { this.contextKeyService.updateParent(data.parentContextKeyService); } if (this.options.configuration.resultEditor.wordWrap === 'on') { - // Intialize the editor with the new proper width so that getContentHeight + // Initialize the editor with the new proper width so that getContentHeight // will be computed correctly in the next call to layout() this.layout(width); } @@ -295,109 +345,13 @@ abstract class BaseCodeBlockPart extends Disposable await this.updateEditor(data); this.layout(width); - this.editor.updateOptions({ ariaLabel: localize('chat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1) }); + this.editor.updateOptions({ ariaLabel: localize('chat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1), readOnly: !editable }); if (data.hideToolbar) { dom.hide(this.toolbar.getElement()); } else { dom.show(this.toolbar.getElement()); } - } - - protected abstract updateEditor(data: Data): void | Promise; - - reset() { - this.clearWidgets(); - } - - private clearWidgets() { - HoverController.get(this.editor)?.hideContentHover(); - } -} - - -export class SimpleCodeBlockPart extends BaseCodeBlockPart { - - private readonly vulnsButton: Button; - private readonly vulnsListElement: HTMLElement; - - private currentCodeBlockData: ISimpleCodeBlockData | undefined; - - private readonly textModel: Promise; - - private readonly _uri: URI; - - constructor( - options: ChatEditorOptions, - menuId: MenuId, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IModelService modelService: IModelService, - @ITextModelService textModelService: ITextModelService, - @IConfigurationService configurationService: IConfigurationService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @ILanguageService private readonly languageService: ILanguageService, - ) { - super(options, menuId, delegate, overflowWidgetsDomNode, instantiationService, contextKeyService, modelService, configurationService, accessibilityService); - - const vulnsContainer = dom.append(this.element, $('.interactive-result-vulns')); - const vulnsHeaderElement = dom.append(vulnsContainer, $('.interactive-result-vulns-header', undefined)); - this.vulnsButton = new Button(vulnsHeaderElement, { - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined, - supportIcons: true - }); - this._uri = URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: generateUuid() }); - this.textModel = textModelService.createModelReference(this._uri).then(ref => { - this.editor.setModel(ref.object.textEditorModel); - this._register(ref); - return ref.object.textEditorModel; - }); - - this.vulnsListElement = dom.append(vulnsContainer, $('ul.interactive-result-vulns-list')); - - this.vulnsButton.onDidClick(() => { - const element = this.currentCodeBlockData!.element as IChatResponseViewModel; - element.vulnerabilitiesListExpanded = !element.vulnerabilitiesListExpanded; - this.vulnsButton.label = this.getVulnerabilitiesLabel(); - this.element.classList.toggle('chat-vulnerabilities-collapsed', !element.vulnerabilitiesListExpanded); - this._onDidChangeContentHeight.fire(); - // this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - }); - } - - get uri(): URI { - return this._uri; - } - - protected override createEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): CodeEditorWidget { - return this._register(instantiationService.createInstance(CodeEditorWidget, parent, options, { - isSimpleWidget: false, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - MenuPreventer.ID, - SelectionClipboardContributionID, - ContextMenuController.ID, - - WordHighlighterContribution.ID, - ViewportSemanticTokensContribution.ID, - BracketMatchingController.ID, - SmartSelectController.ID, - HoverController.ID, - GotoDefinitionAtPositionEditorContribution.ID, - ]) - })); - } - - override async render(data: ISimpleCodeBlockData, width: number): Promise { - await super.render(data, width); if (data.vulns?.length && isResponseVM(data.element)) { dom.clearNode(this.vulnsListElement); @@ -410,20 +364,27 @@ export class SimpleCodeBlockPart extends BaseCodeBlockPart } } - protected override async updateEditor(data: ISimpleCodeBlockData): Promise { - this.editor.setModel(await this.textModel); - const text = this.fixCodeText(data.text, data.languageId); - this.setText(text); + reset() { + this.clearWidgets(); + } + + private clearWidgets() { + HoverController.get(this.editor)?.hideContentHover(); + } - const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(data.languageId) ?? undefined; - this.setLanguage(vscodeLanguageId); - data.languageId = vscodeLanguageId ?? 'plaintext'; + private async updateEditor(data: ICodeBlockData): Promise { + const textModel = (await data.textModel).object.textEditorModel; + this.editor.setModel(textModel); + if (data.range) { + this.editor.setSelection(data.range); + this.editor.revealRangeInCenter(data.range, ScrollType.Immediate); + } this.toolbar.context = { - code: data.text, + code: textModel.getTextBuffer().getValueInRange(data.range ?? textModel.getFullModelRange(), EndOfLinePreference.TextDefined), codeBlockIndex: data.codeBlockIndex, element: data.element, - languageId: data.languageId + languageId: textModel.getLanguageId() } satisfies ICodeBlockActionContext; } @@ -438,110 +399,8 @@ export class SimpleCodeBlockPart extends BaseCodeBlockPart const icon = (element: IChatResponseViewModel) => element.vulnerabilitiesListExpanded ? Codicon.chevronDown : Codicon.chevronRight; return `${referencesLabel} $(${icon(this.currentCodeBlockData.element as IChatResponseViewModel).id})`; } - - private fixCodeText(text: string, languageId: string): string { - if (languageId === 'php') { - if (!text.trim().startsWith('<')) { - return ``; - } - } - - return text; - } - - private async setText(newText: string): Promise { - const model = await this.textModel; - const currentText = model.getValue(EndOfLinePreference.LF); - if (newText === currentText) { - return; - } - - if (newText.startsWith(currentText)) { - const text = newText.slice(currentText.length); - const lastLine = model.getLineCount(); - const lastCol = model.getLineMaxColumn(lastLine); - model.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]); - } else { - // console.log(`Failed to optimize setText`); - model.setValue(newText); - } - } - - private async setLanguage(vscodeLanguageId: string | undefined): Promise { - (await this.textModel).setLanguage(vscodeLanguageId ?? PLAINTEXT_LANGUAGE_ID); - } } -export class LocalFileCodeBlockPart extends BaseCodeBlockPart { - - private readonly textModelReference = this._register(new MutableDisposable>()); - private currentCodeBlockData?: ILocalFileCodeBlockData; - - constructor( - options: ChatEditorOptions, - menuId: MenuId, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IModelService modelService: IModelService, - @ITextModelService private readonly textModelService: ITextModelService, - @IConfigurationService configurationService: IConfigurationService, - @IAccessibilityService accessibilityService: IAccessibilityService - ) { - super(options, menuId, delegate, overflowWidgetsDomNode, instantiationService, contextKeyService, modelService, configurationService, accessibilityService); - } - - get uri(): URI { - return this.currentCodeBlockData!.uri; - } - - protected override getContentHeight() { - if (this.currentCodeBlockData?.range) { - const lineCount = this.currentCodeBlockData.range.endLineNumber - this.currentCodeBlockData.range.startLineNumber + 1; - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - return lineCount * lineHeight; - } - return super.getContentHeight(); - } - - protected override createEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): CodeEditorWidget { - return this._register(instantiationService.createInstance(CodeEditorWidget, parent, { - ...options, - }, { - // TODO: be more selective about contributions - })); - } - - protected override async updateEditor(data: ILocalFileCodeBlockData): Promise { - let model: ITextModel; - if (this.currentCodeBlockData?.uri.toString() === data.uri.toString()) { - this.currentCodeBlockData = data; - model = this.editor.getModel()!; - } else { - this.currentCodeBlockData = data; - const result = await this.textModelService.createModelReference(data.uri); - model = result.object.textEditorModel; - this.textModelReference.value = result; - this.editor.setModel(model); - } - - - if (data.range) { - this.editor.setSelection(data.range); - this.editor.revealRangeInCenter(data.range, ScrollType.Immediate); - } - - this.toolbar.context = { - code: model.getTextBuffer().getValueInRange(data.range ?? model.getFullModelRange(), EndOfLinePreference.TextDefined), - codeBlockIndex: data.codeBlockIndex, - element: data.element, - languageId: model.getLanguageId() - } satisfies ICodeBlockActionContext; - } -} - - export class ChatCodeBlockContentProvider extends Disposable implements ITextModelContentProvider { constructor( diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 7b6bb6c1c2b..43864b83521 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -11,14 +11,16 @@ import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; +import { AnythingQuickAccessProviderRunOptions, IQuickAccessOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { IChatRequestVariableValue, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatRequestVariableValue, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; export const dynamicVariableDecorationType = 'chat-dynamic-variable'; @@ -135,6 +137,8 @@ export class SelectAndInsertFileAction extends Action2 { async run(accessor: ServicesAccessor, ...args: any[]) { const textModelService = accessor.get(ITextModelService); const logService = accessor.get(ILogService); + const quickInputService = accessor.get(IQuickInputService); + const chatVariablesService = accessor.get(IChatVariablesService); const context = args[0]; if (!isSelectAndInsertFileActionContext(context)) { @@ -146,14 +150,45 @@ export class SelectAndInsertFileAction extends Action2 { context.widget.inputEditor.executeEdits('chatInsertFile', [{ range: context.range, text: `` }]); }; - const quickInputService = accessor.get(IQuickInputService); - const picks = await quickInputService.quickAccess.pick(''); + let options: IQuickAccessOptions | undefined; + const filesVariableName = 'files'; + const filesItem = { + label: localize('allFiles', 'All Files'), + description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), + }; + // If we have a `files` variable, add an option to select all files in the picker. + // This of course assumes that the `files` variable has the behavior that it searches + // through files in the workspace. + if (chatVariablesService.hasVariable(filesVariableName)) { + options = { + providerOptions: { + additionPicks: [filesItem, { type: 'separator' }] + }, + }; + } + // TODO: have dedicated UX for this instead of using the quick access picker + const picks = await quickInputService.quickAccess.pick('', options); if (!picks?.length) { logService.trace('SelectAndInsertFileAction: no file selected'); doCleanup(); return; } + const editor = context.widget.inputEditor; + const range = context.range; + + // Handle the special case of selecting all files + if (picks[0] === filesItem) { + const text = `#${filesVariableName}`; + const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); + if (!success) { + logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); + doCleanup(); + } + return; + } + + // Handle the case of selecting a specific file const resource = (picks[0] as unknown as { resource: unknown }).resource as URI; if (!textModelService.canHandleResource(resource)) { logService.trace('SelectAndInsertFileAction: non-text resource selected'); @@ -162,9 +197,7 @@ export class SelectAndInsertFileAction extends Action2 { } const fileName = basename(resource); - const editor = context.widget.inputEditor; const text = `#file:${fileName}`; - const range = context.range; const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); if (!success) { logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 791f9e41fd3..eff2859569b 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -27,7 +26,6 @@ import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { SelectAndInsertFileAction, dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; -import { getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -126,8 +124,7 @@ class InputEditorDecorations extends Disposable { } if (!inputValue) { - const viewModelPlaceholder = this.widget.viewModel?.inputPlaceholder; - const placeholder = viewModelPlaceholder ?? ''; + const defaultAgent = this.chatAgentService.getDefaultAgent(); const decoration: IDecorationOptions[] = [ { range: { @@ -138,7 +135,7 @@ class InputEditorDecorations extends Disposable { }, renderOptions: { after: { - contentText: placeholder, + contentText: viewModel.inputPlaceholder ?? defaultAgent?.metadata.description ?? '', color: this.getPlaceholderColor() } } @@ -348,7 +345,7 @@ class AgentCompletions extends Disposable { } const agents = this.chatAgentService.getAgents() - .filter(a => !a.metadata.isDefault); + .filter(a => !a.isDefault); return { suggestions: agents.map((c, i) => { const withAt = `@${c.id}`; @@ -399,10 +396,8 @@ class AgentCompletions extends Disposable { } const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - const commands = await usedAgent.agent.provideSlashCommands(widget.viewModel.model, getHistoryEntriesFromModel(widget.viewModel.model), token); // Refresh the cache here - return { - suggestions: commands.map((c, i) => { + suggestions: usedAgent.agent.slashCommands.map((c, i) => { const withSlash = `/${c.name}`; return { label: withSlash, @@ -433,15 +428,8 @@ class AgentCompletions extends Disposable { } const agents = this.chatAgentService.getAgents(); - const all = agents.map(agent => agent.provideSlashCommands(viewModel.model, getHistoryEntriesFromModel(viewModel.model), token)); - const commands = await raceCancellation(Promise.all(all), token); - - if (!commands) { - return; - } - const justAgents: CompletionItem[] = agents - .filter(a => !a.metadata.isDefault) + .filter(a => !a.isDefault) .map(agent => { const agentLabel = `${chatAgentLeader}${agent.id}`; return { @@ -456,7 +444,7 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( - agents.flatMap((agent, i) => commands[i].map((c, i) => { + agents.flatMap(agent => agent.slashCommands.map((c, i) => { const agentLabel = `${chatAgentLeader}${agent.id}`; const withSlash = `${chatSubcommandLeader}${c.name}`; return { @@ -464,7 +452,7 @@ class AgentCompletions extends Disposable { filterText: `${chatSubcommandLeader}${agent.id}${c.name}`, commitCharacters: [' '], insertText: `${agentLabel} ${withSlash} `, - detail: `(${agentLabel}) ${c.description}`, + detail: `(${agentLabel}) ${c.description ?? ''}`, range: new Range(1, 1, 1, 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway sortText: `${chatSubcommandLeader}${agent.id}${c.name}`, @@ -607,7 +595,7 @@ class ChatTokenDeleter extends Disposable { // A simple heuristic to delete the previous token when the user presses backspace. // The sophisticated way to do this would be to have a parse tree that can be updated incrementally. - this.widget.inputEditor.onDidChangeModelContent(e => { + this._register(this.widget.inputEditor.onDidChangeModelContent(e => { if (!previousInputValue) { previousInputValue = inputValue; } @@ -637,7 +625,7 @@ class ChatTokenDeleter extends Disposable { } previousInputValue = this.widget.inputEditor.getValue(); - }); + })); } } ChatWidget.CONTRIBS.push(ChatTokenDeleter); diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chat.css b/code/src/vs/workbench/contrib/chat/browser/media/chat.css index 20f6f5d621f..eff556a5152 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -338,14 +338,13 @@ 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: 4px; position: relative; padding: 0 6px; margin-bottom: 4px; - align-items: center; + align-items: flex-end; justify-content: space-between; } @@ -359,6 +358,10 @@ border-color: var(--vscode-focusBorder); } +.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .mtk1 { + color: var(--vscode-input-foreground); +} + .interactive-session .interactive-input-and-execute-toolbar .monaco-editor, .interactive-session .interactive-input-and-execute-toolbar .monaco-editor .monaco-editor-background { background-color: var(--vscode-input-background) !important; @@ -370,6 +373,14 @@ .interactive-session .interactive-input-part .interactive-execute-toolbar { height: 22px; + + /* It's bottom-aligned, make it appear centered within the container */ + margin-bottom: 7px; +} + +.interactive-session .interactive-input-part .interactive-execute-toolbar .monaco-action-bar .actions-container { + display: flex; + gap: 4px; } .interactive-session .interactive-input-part .interactive-execute-toolbar .codicon-debug-stop { @@ -413,11 +424,19 @@ } .interactive-session .interactive-input-part { + margin: 0px 20px; padding: 12px 0px; display: flex; flex-direction: column; } +.interactive-session .chat-implicit-context { + padding: 8px 8px 13px; + margin-bottom: -5px; + border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); + border-radius: 6px 6px 0px 0px; +} + .interactive-session-followups { display: flex; flex-direction: column; @@ -439,10 +458,6 @@ padding: 4px 8px; } -.interactive-session .interactive-input-part .interactive-input-followups { - margin: 0px 20px; -} - .interactive-session .interactive-input-part .interactive-input-followups .interactive-session-followups { margin-bottom: 8px; } @@ -489,7 +504,7 @@ } .quick-input-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { - bottom: 1px; + margin-bottom: 1px; } .quick-input-widget .interactive-session .interactive-input-and-execute-toolbar { diff --git a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts index 4ee4c07f0cf..5815a725469 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isNonEmptyArray, distinct } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -11,9 +12,11 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatContributionService, IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc @@ -24,51 +27,59 @@ export interface IChatAgentHistoryEntry { result: IChatAgentResult; } +export enum ChatAgentLocation { + Panel = 1, + Terminal = 2, + Notebook = 3, + // Editor = 4 +} + +export namespace ChatAgentLocation { + export function fromRaw(value: RawChatParticipantLocation | string): ChatAgentLocation { + switch (value) { + case 'panel': return ChatAgentLocation.Panel; + case 'terminal': return ChatAgentLocation.Terminal; + case 'notebook': return ChatAgentLocation.Notebook; + } + return ChatAgentLocation.Panel; + } +} + export interface IChatAgentData { id: string; extensionId: ExtensionIdentifier; + /** The agent invoked when no agent is specified */ + isDefault?: boolean; metadata: IChatAgentMetadata; + slashCommands: IChatAgentCommand[]; + defaultImplicitVariables?: string[]; + locations: ChatAgentLocation[]; } -export interface IChatAgent extends IChatAgentData { +export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined; - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; provideSampleQuestions?(token: CancellationToken): ProviderResult; } -export interface IChatAgentCommand { - name: string; - description: string; +export type IChatAgent = IChatAgentData & IChatAgentImplementation; - /** - * Whether the command should execute as soon - * as it is entered. Defaults to `false`. - */ - executeImmediately?: boolean; +export interface IChatAgentCommand extends IRawChatCommandContribution { + followupPlaceholder?: string; +} - /** - * Whether executing the command puts the - * chat into a persistent mode, where the - * slash command is prepended to the chat input. - */ - isSticky?: boolean; +export interface IChatRequesterInformation { + name: string; /** - * Placeholder text to render in the chat input - * when the slash command has been repopulated. - * Has no effect if `shouldRepopulate` is `false`. + * A full URI for the icon of the requester. */ - followupPlaceholder?: string; - - sampleRequest?: string; + icon?: URI; } export interface IChatAgentMetadata { description?: string; - isDefault?: boolean; // The agent invoked when no agent is specified helpTextPrefix?: string | IMarkdownString; helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; @@ -81,6 +92,7 @@ export interface IChatAgentMetadata { supportIssueReporting?: boolean; followupPlaceholder?: string; isSticky?: boolean; + requester?: IChatRequesterInformation; } @@ -91,6 +103,7 @@ export interface IChatAgentRequest { command?: string; message: string; variables: IChatRequestVariableData; + location: ChatAgentLocation; } export interface IChatAgentResult { @@ -107,15 +120,20 @@ export const IChatAgentService = createDecorator('chatAgentSe export interface IChatAgentService { _serviceBrand: undefined; - readonly onDidChangeAgents: Event; - registerAgent(agent: IChatAgent): IDisposable; + /** + * undefined when an agent was removed IChatAgent + */ + readonly onDidChangeAgents: Event; + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getAgents(): Array; - getAgent(id: string): IChatAgent | undefined; + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + getAgents(): IChatAgentData[]; + getRegisteredAgents(): Array; + getActivatedAgents(): Array; + getAgent(id: string): IChatAgentData | undefined; getDefaultAgent(): IChatAgent | undefined; - getSecondaryAgent(): IChatAgent | undefined; - hasAgent(id: string): boolean; + getSecondaryAgent(): IChatAgentData | undefined; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } @@ -125,79 +143,178 @@ export class ChatAgentService extends Disposable implements IChatAgentService { declare _serviceBrand: undefined; - private readonly _agents = new Map(); + private readonly _agents = new Map(); + + private readonly _onDidChangeAgents = this._register(new Emitter()); + readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; - private readonly _onDidChangeAgents = this._register(new Emitter()); - readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + constructor( + @IChatContributionService private chatContributionService: IChatContributionService, + @IContextKeyService private contextKeyService: IContextKeyService, + ) { + super(); + } override dispose(): void { super.dispose(); this._agents.clear(); } - registerAgent(agent: IChatAgent): IDisposable { - if (this._agents.has(agent.id)) { - throw new Error(`Already registered an agent with id ${agent.id}`); + registerAgent(name: string, agentImpl: IChatAgentImplementation): IDisposable { + if (this._agents.has(name)) { + // TODO not keyed by name, dupes allowed between extensions + throw new Error(`Already registered an agent with id ${name}`); + } + + const data = this.getAgent(name); + if (!data) { + throw new Error(`Unknown agent: ${name}`); } - this._agents.set(agent.id, { agent }); - this._onDidChangeAgents.fire(); + + const agent = { data: data, impl: agentImpl }; + this._agents.set(name, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); return toDisposable(() => { - if (this._agents.delete(agent.id)) { - this._onDidChangeAgents.fire(); + if (this._agents.delete(name)) { + this._onDidChangeAgents.fire(undefined); + } + }); + } + + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { + const agent = { data, impl: agentImpl }; + this._agents.set(data.id, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); + + return toDisposable(() => { + if (this._agents.delete(data.id)) { + this._onDidChangeAgents.fire(undefined); } }); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { - const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id} registered`); + const agent = this._agents.get(id); + if (!agent?.impl) { + throw new Error(`No activated agent with id ${id} registered`); } - data.agent.metadata = { ...data.agent.metadata, ...updateMetadata }; - this._onDidChangeAgents.fire(); + agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; + this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); } getDefaultAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isDefault)?.agent; + return this.getActivatedAgents().find(a => !!a.isDefault); + } + + getSecondaryAgent(): IChatAgentData | undefined { + // TODO also static + return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - getSecondaryAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isSecondary)?.agent; + getRegisteredAgents(): Array { + const that = this; + return this.chatContributionService.registeredParticipants.map(p => ( + { + extensionId: p.extensionId, + id: p.name, + metadata: { description: p.description }, + isDefault: p.isDefault, + defaultImplicitVariables: p.defaultImplicitVariables, + locations: isNonEmptyArray(p.locations) ? p.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel], + get slashCommands() { + const commands = p.commands ?? []; + return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); + } + } satisfies IChatAgentData)); } - getAgents(): Array { - return Array.from(this._agents.values(), v => v.agent); + /** + * Returns all agent datas that exist- static registered and dynamic ones. + */ + getAgents(): IChatAgentData[] { + const registeredAgents = this.getRegisteredAgents(); + const dynamicAgents = Array.from(this._agents.values()).map(a => a.data); + const all = [ + ...registeredAgents, + ...dynamicAgents + ]; + + return distinct(all, a => a.id); } - hasAgent(id: string): boolean { - return this._agents.has(id); + getActivatedAgents(): IChatAgent[] { + return Array.from(this._agents.values()) + .filter(a => !!a.impl) + .map(a => new MergedChatAgent(a.data, a.impl!)); } - getAgent(id: string): IChatAgent | undefined { - const data = this._agents.get(id); - return data?.agent; + getAgent(id: string): IChatAgentData | undefined { + return this.getAgents().find(a => a.id === id); } async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - return await data.agent.invoke(request, progress, history, token); + return await data.impl.invoke(request, progress, history, token); } - async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - if (!data.agent.provideFollowups) { + if (!data.impl?.provideFollowups) { return []; } - return data.agent.provideFollowups(request, result, token); + return data.impl.provideFollowups(request, result, history, token); + } +} + +export class MergedChatAgent implements IChatAgent { + constructor( + private readonly data: IChatAgentData, + private readonly impl: IChatAgentImplementation + ) { } + + get id(): string { return this.data.id; } + get extensionId(): ExtensionIdentifier { return this.data.extensionId; } + get isDefault(): boolean | undefined { return this.data.isDefault; } + get metadata(): IChatAgentMetadata { return this.data.metadata; } + get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } + get defaultImplicitVariables(): string[] | undefined { return this.data.defaultImplicitVariables; } + get locations(): ChatAgentLocation[] { return this.data.locations; } + + async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + return this.impl.invoke(request, progress, history, token); + } + + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + if (this.impl.provideFollowups) { + return this.impl.provideFollowups(request, result, history, token); + } + + return []; + } + + provideWelcomeMessage(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { + if (this.impl.provideWelcomeMessage) { + return this.impl.provideWelcomeMessage(token); + } + + return undefined; + } + + provideSampleQuestions(token: CancellationToken): ProviderResult { + if (this.impl.provideSampleQuestions) { + return this.impl.provideSampleQuestions(token); + } + + return undefined; } } diff --git a/code/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/code/src/vs/workbench/contrib/chat/common/chatContributionService.ts index 2c43c2af703..b0e4facf10c 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IChatProviderContribution { @@ -18,6 +19,10 @@ export interface IChatContributionService { registerChatProvider(provider: IChatProviderContribution): void; deregisterChatProvider(providerId: string): void; getViewIdForProvider(providerId: string): string; + + readonly registeredParticipants: IChatParticipantContribution[]; + registerChatParticipant(participant: IChatParticipantContribution): void; + deregisterChatParticipant(participant: IChatParticipantContribution): void; } export interface IRawChatProviderContribution { @@ -26,3 +31,28 @@ export interface IRawChatProviderContribution { icon?: string; when?: string; } + +export interface IRawChatCommandContribution { + name: string; + description: string; + sampleRequest?: string; + isSticky?: boolean; + when?: string; + defaultImplicitVariables?: string[]; +} + +export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook'; + +export interface IRawChatParticipantContribution { + name: string; + description?: string; + isDefault?: boolean; + commands?: IRawChatCommandContribution[]; + defaultImplicitVariables?: string[]; + locations?: RawChatParticipantLocation[]; +} + +export interface IChatParticipantContribution extends IRawChatParticipantContribution { + // Participant id is extensionId + name + extensionId: ExtensionIdentifier; +} diff --git a/code/src/vs/workbench/contrib/chat/common/chatModel.ts b/code/src/vs/workbench/contrib/chat/common/chatModel.ts index c8e47cb8b16..1cd78337dab 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,19 +10,31 @@ import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/commo import { Disposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { basename } from 'vs/base/common/resources'; -import { URI, UriComponents, UriDto } from 'vs/base/common/uri'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI, UriComponents, UriDto, isUriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; -export interface IChatRequestVariableData { +export interface IChatPromptVariableData { variables: { name: string; range: IOffsetRange; values: IChatRequestVariableValue[] }[]; } +export interface IChatRequestVariableEntry { + name: string; + range?: IOffsetRange; + values: IChatRequestVariableValue[]; +} + +export interface IChatRequestVariableData { + variables: IChatRequestVariableEntry[]; +} + export interface IChatRequestModel { readonly id: string; readonly username: string; @@ -54,7 +66,7 @@ export interface IChatResponseModel { readonly providerId: string; readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon | URI; readonly session: IChatModel; readonly agent?: IChatAgentData; readonly usedContext: IChatUsedContext | undefined; @@ -234,8 +246,8 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this.session.responderUsername; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | URI | undefined { + return this.session.responderAvatarIcon; } private _followups?: IChatFollowup[]; @@ -392,7 +404,7 @@ export interface IExportableChatData { requesterUsername: string; responderUsername: string; requesterAvatarIconUri: UriComponents | undefined; - responderAvatarIconUri: UriComponents | undefined; + responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat } export interface ISerializableChatData extends IExportableChatData { @@ -405,8 +417,7 @@ export function isExportableSessionData(obj: unknown): obj is IExportableChatDat const data = obj as IExportableChatData; return typeof data === 'object' && typeof data.providerId === 'string' && - typeof data.requesterUsername === 'string' && - typeof data.responderUsername === 'string'; + typeof data.requesterUsername === 'string'; } export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData { @@ -483,10 +494,6 @@ export class ChatModel extends Disposable implements IChatModel { return this._sessionId; } - get inputPlaceholder(): string | undefined { - return this._session?.inputPlaceholder; - } - get requestInProgress(): boolean { const lastRequest = this._requests[this._requests.length - 1]; return !!lastRequest && !!lastRequest.response && !lastRequest.response.isComplete; @@ -497,22 +504,34 @@ export class ChatModel extends Disposable implements IChatModel { return this._creationDate; } + private get _defaultAgent() { + return this.chatAgentService.getDefaultAgent(); + } + get requesterUsername(): string { - return this._session?.requesterUsername ?? this.initialData?.requesterUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.requester?.name : + this.initialData?.requesterUsername) ?? ''; } get responderUsername(): string { - return this._session?.responderUsername ?? this.initialData?.responderUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.fullName : + this.initialData?.responderUsername) ?? ''; } private readonly _initialRequesterAvatarIconUri: URI | undefined; get requesterAvatarIconUri(): URI | undefined { - return this._session ? this._session.requesterAvatarIconUri : this._initialRequesterAvatarIconUri; + return this._defaultAgent ? + this._defaultAgent.metadata.requester?.icon : + this._initialRequesterAvatarIconUri; } - private readonly _initialResponderAvatarIconUri: URI | undefined; - get responderAvatarIconUri(): URI | undefined { - return this._session ? this._session.responderAvatarIconUri : this._initialResponderAvatarIconUri; + private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; + get responderAvatarIcon(): ThemeIcon | URI | undefined { + return this._defaultAgent ? + this._defaultAgent?.metadata.themeIcon : + this._initialResponderAvatarIconUri; } get initState(): ChatModelInitState { @@ -533,6 +552,7 @@ export class ChatModel extends Disposable implements IChatModel { private readonly initialData: ISerializableChatData | IExportableChatData | undefined, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -542,7 +562,7 @@ export class ChatModel extends Disposable implements IChatModel { this._creationDate = (isSerializableSessionData(initialData) && initialData.creationDate) || Date.now(); this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); - this._initialResponderAvatarIconUri = initialData?.responderAvatarIconUri && URI.revive(initialData.responderAvatarIconUri); + this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { @@ -554,7 +574,7 @@ export class ChatModel extends Disposable implements IChatModel { if (obj.welcomeMessage) { const content = obj.welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item); - this._welcomeMessage = new ChatWelcomeMessageModel(this, content, []); + this._welcomeMessage = this.instantiationService.createInstance(ChatWelcomeMessageModel, content, []); } try { @@ -748,7 +768,7 @@ export class ChatModel extends Disposable implements IChatModel { requesterUsername: this.requesterUsername, requesterAvatarIconUri: this.requesterAvatarIconUri, responderUsername: this.responderUsername, - responderAvatarIconUri: this.responderAvatarIconUri, + responderAvatarIconUri: this.responderAvatarIcon, welcomeMessage: this._welcomeMessage?.content.map(c => { if (Array.isArray(c)) { return c; @@ -780,7 +800,10 @@ export class ChatModel extends Disposable implements IChatModel { followups: r.response?.followups, isCanceled: r.response?.isCanceled, vote: r.response?.vote, - agent: r.response?.agent ? { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata } : undefined, // May actually be the full IChatAgent instance, just take the data props + agent: r.response?.agent ? + // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. + { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault } + : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences @@ -815,7 +838,7 @@ export interface IChatWelcomeMessageModel { readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon; } @@ -828,19 +851,19 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } constructor( - private readonly session: ChatModel, public readonly content: IChatWelcomeMessageContent[], - public readonly sampleQuestions: IChatFollowup[] + public readonly sampleQuestions: IChatFollowup[], + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { this._id = 'welcome_' + ChatWelcomeMessageModel.nextId++; } public get username(): string { - return this.session.responderUsername; + return this.chatAgentService.getDefaultAgent()?.metadata.fullName ?? ''; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | undefined { + return this.chatAgentService.getDefaultAgent()?.metadata.themeIcon; } } @@ -858,7 +881,8 @@ export function getHistoryEntriesFromModel(model: IChatModel): IChatAgentHistory agentId: request.response.agent?.id ?? '', message: promptTextResult.message, command: request.response.slashCommand?.name, - variables: updateRanges(request.variableData, promptTextResult.diff) // TODO bit of a hack + variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack + location: ChatAgentLocation.Panel }; history.push({ request: historyRequest, response: request.response.response.value, result: request.response.result ?? {} }); } @@ -870,7 +894,7 @@ export function updateRanges(variableData: IChatRequestVariableData, diff: numbe return { variables: variableData.variables.map(v => ({ ...v, - range: { + range: v.range && { start: v.range.start - diff, endExclusive: v.range.endExclusive - diff } diff --git a/code/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/code/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 4f7654fd56a..59f8b873cbb 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgent, IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -72,7 +72,7 @@ export class ChatRequestVariablePart implements IParsedChatRequestPart { export class ChatRequestAgentPart implements IParsedChatRequestPart { static readonly Kind = 'agent'; readonly kind = ChatRequestAgentPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgent) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } get text(): string { return `${chatAgentLeader}${this.agent.id}`; @@ -197,3 +197,9 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed }) }; } + +export function extractAgentAndCommand(parsed: IParsedChatRequest): { agentPart: ChatRequestAgentPart | undefined; commandPart: ChatRequestAgentSubcommandPart | undefined } { + const agentPart = parsed.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); + const commandPart = parsed.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); + return { agentPart, commandPart }; +} diff --git a/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index aa05f52b6d8..52c683f60a4 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -171,8 +171,7 @@ export class ChatRequestParser { return; } - const subCommands = usedAgent.agent.getLastSlashCommands(model); - const subCommand = subCommands?.find(c => c.name === command); + const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); if (subCommand) { // Valid agent subcommand return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); diff --git a/code/src/vs/workbench/contrib/chat/common/chatService.ts b/code/src/vs/workbench/contrib/chat/common/chatService.ts index 5da8d9b747b..cd113c0dc7b 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatService.ts @@ -19,11 +19,6 @@ import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chat export interface IChat { id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about - requesterUsername: string; - requesterAvatarIconUri?: URI; - responderUsername: string; - responderAvatarIconUri?: URI; - inputPlaceholder?: string; dispose?(): void; } @@ -288,12 +283,11 @@ export interface IChatService { /** * Returns whether the request was accepted. */ - sendRequest(sessionId: string, message: string): Promise; + sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean): Promise; removeRequest(sessionid: string, requestId: string): Promise; cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void; - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void; getHistory(): IChatDetail[]; clearAllHistoryEntries(): void; removeHistoryEntry(sessionId: string): void; diff --git a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 6d89b981afc..022800fbfc7 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; @@ -20,15 +21,15 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -366,7 +367,7 @@ export class ChatService extends Disposable implements IChatService { const provider = this._providers.get(model.providerId); if (!provider) { - throw new Error(`Unknown provider: ${model.providerId}`); + throw new ErrorNoTelemetry(`Unknown provider: ${model.providerId}`); } let session: IChat | undefined; @@ -384,12 +385,12 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getDefaultAgent(); if (!defaultAgent) { - throw new Error('No default agent'); + throw new ErrorNoTelemetry('No default agent'); } const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(token) ?? undefined; - const welcomeModel = welcomeMessage && new ChatWelcomeMessageModel( - model, + const welcomeModel = welcomeMessage && this.instantiationService.createInstance( + ChatWelcomeMessageModel, welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item), await defaultAgent.provideSampleQuestions?.(token) ?? [] ); @@ -434,7 +435,7 @@ export class ChatService extends Disposable implements IChatService { return this._startSession(data.providerId, data, CancellationToken.None); } - async sendRequest(sessionId: string, request: string): Promise { + async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); if (!request.trim()) { this.trace('sendRequest', 'Rejected empty message'); @@ -463,7 +464,7 @@ export class ChatService extends Disposable implements IChatService { // This method is only returning whether the request was accepted - don't block on the actual request return { - responseCompletePromise: this._sendRequestAsync(model, sessionId, provider, parsedRequest), + responseCompletePromise: this._sendRequestAsync(model, sessionId, provider, parsedRequest, implicitVariablesEnabled), agent, slashCommand: agentSlashCommandPart?.command, }; @@ -477,7 +478,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, parsedRequest: IParsedChatRequest): Promise { + private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, parsedRequest: IParsedChatRequest, implicitVariablesEnabled?: boolean): Promise { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -531,6 +532,7 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getDefaultAgent(); if (agentPart || (defaultAgent && !commandPart)) { const agent = (agentPart?.agent ?? defaultAgent)!; + await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); const history = getHistoryEntriesFromModel(model); const initVariableData: IChatRequestVariableData = { variables: [] }; @@ -539,18 +541,28 @@ export class ChatService extends Disposable implements IChatService { request.variableData = variableData; const promptTextResult = getPromptText(request.message); + const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack + if (implicitVariablesEnabled) { + const implicitVariables = agent.defaultImplicitVariables; + if (implicitVariables) { + const resolvedImplicitVariables = await Promise.all(implicitVariables.map(async v => ({ name: v, values: await this.chatVariablesService.resolveVariable(v, parsedRequest.text, model, progressCallback, token) } satisfies IChatRequestVariableEntry))); + updatedVariableData.variables.push(...resolvedImplicitVariables); + } + } + const requestProps: IChatAgentRequest = { sessionId, requestId: request.id, agentId: agent.id, message: promptTextResult.message, command: agentSlashCommandPart?.command.name, - variables: updateRanges(variableData, promptTextResult.diff) // TODO bit of a hack + variables: updatedVariableData, + location: ChatAgentLocation.Terminal }; const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); rawResult = agentResult; - agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, followupsCancelToken); + agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { request = model.addRequest(parsedRequest, { variables: [] }); // contributed slash commands @@ -633,11 +645,6 @@ export class ChatService extends Disposable implements IChatService { model.removeRequest(requestId); } - async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<{ responseCompletePromise: Promise } | undefined> { - this.trace('sendRequestToProvider', `sessionId: ${sessionId}`); - return await this.sendRequest(sessionId, message.message); - } - getProviders(): string[] { return Array.from(this._providers.keys()); } diff --git a/code/src/vs/workbench/contrib/chat/common/chatVariables.ts b/code/src/vs/workbench/contrib/chat/common/chatVariables.ts index 859daf171e5..fb6eb71a858 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -48,6 +48,7 @@ export interface IChatVariablesService { * Resolves all variables that occur in `prompt` */ resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; + resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; } export interface IDynamicVariable { diff --git a/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts index a91512a6840..9fcd50c8214 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -5,14 +5,20 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { marked } from 'vs/base/common/marked/marked'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { EndOfLinePreference } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatContentReference, IChatProgressMessage, IChatFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection, IChatCommandButton } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; +import { CodeBlockModelCollection } from './codeBlockModelCollection'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { return !!item && typeof item === 'object' && 'message' in item; @@ -60,7 +66,7 @@ export interface IChatRequestViewModel { /** This ID updates every time the underlying data changes */ readonly dataId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; currentRenderedHeight: number | undefined; @@ -110,7 +116,7 @@ export interface IChatResponseViewModel { /** The ID of the associated IChatRequestViewModel */ readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly agent?: IChatAgentData; readonly slashCommand?: IChatAgentCommand; readonly response: IResponse; @@ -134,6 +140,7 @@ export interface IChatResponseViewModel { } export class ChatViewModel extends Disposable implements IChatViewModel { + private readonly _onDidDisposeModel = this._register(new Emitter()); readonly onDidDisposeModel = this._onDidDisposeModel.event; @@ -144,7 +151,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private _inputPlaceholder: string | undefined = undefined; get inputPlaceholder(): string | undefined { - return this._inputPlaceholder ?? this._model.inputPlaceholder; + return this._inputPlaceholder; } get model(): IChatModel { @@ -179,12 +186,17 @@ export class ChatViewModel extends Disposable implements IChatViewModel { constructor( private readonly _model: IChatModel, + public readonly codeBlockModelCollection: CodeBlockModelCollection, @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILanguageService private readonly languageService: ILanguageService, ) { super(); _model.getRequests().forEach((request, i) => { - this._items.push(new ChatRequestViewModel(request)); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, request); + this._items.push(requestModel); + this.updateCodeBlockTextModels(requestModel); + if (request.response) { this.onAddResponse(request.response); } @@ -193,7 +205,10 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._register(_model.onDidDispose(() => this._onDidDisposeModel.fire())); this._register(_model.onDidChange(e => { if (e.kind === 'addRequest') { - this._items.push(new ChatRequestViewModel(e.request)); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, e.request); + this._items.push(requestModel); + this.updateCodeBlockTextModels(requestModel); + if (e.request.response) { this.onAddResponse(e.request.response); } @@ -224,8 +239,12 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private onAddResponse(responseModel: IChatResponseModel) { const response = this.instantiationService.createInstance(ChatResponseViewModel, responseModel); - this._register(response.onDidChange(() => this._onDidChange.fire(null))); + this._register(response.onDidChange(() => { + this.updateCodeBlockTextModels(response); + return this._onDidChange.fire(null); + })); this._items.push(response); + this.updateCodeBlockTextModels(response); } getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel)[] { @@ -238,6 +257,79 @@ export class ChatViewModel extends Disposable implements IChatViewModel { .filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel) .forEach((item: ChatResponseViewModel) => item.dispose()); } + + updateCodeBlockTextModels(model: IChatRequestViewModel | IChatResponseViewModel) { + const content = isRequestVM(model) ? model.messageText : model.response.asString(); + const renderer = new marked.Renderer(); + + let codeBlockIndex = 0; + renderer.code = (value, languageId) => { + languageId ??= ''; + const newText = this.fixCodeText(value, languageId); + const textModel = this.codeBlockModelCollection.getOrCreate(this._model.sessionId, model, codeBlockIndex++); + textModel.then(ref => { + const model = ref.object.textEditorModel; + if (languageId) { + const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(languageId); + if (vscodeLanguageId && vscodeLanguageId !== ref.object.textEditorModel.getLanguageId()) { + ref.object.textEditorModel.setLanguage(vscodeLanguageId); + } + } + + const currentText = ref.object.textEditorModel.getValue(EndOfLinePreference.LF); + if (newText === currentText) { + return; + } + + if (newText.startsWith(currentText)) { + const text = newText.slice(currentText.length); + const lastLine = model.getLineCount(); + const lastCol = model.getLineMaxColumn(lastLine); + model.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]); + } else { + // console.log(`Failed to optimize setText`); + model.setValue(newText); + } + }); + return ''; + }; + + marked.parse(this.ensureFencedCodeBlocksTerminated(content), { renderer }); + } + + private fixCodeText(text: string, languageId: string): string { + if (languageId === 'php') { + if (!text.trim().startsWith('<')) { + return ``; + } + } + + return text; + } + + /** + * Marked doesn't consistently render fenced code blocks that aren't terminated. + * + * Try to close them ourselves to workaround this. + */ + private ensureFencedCodeBlocksTerminated(content: string): string { + const lines = content.split('\n'); + let inCodeBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('```')) { + inCodeBlock = !inCodeBlock; + } + } + + // If we're still in a code block at the end of the content, add a closing fence + if (inCodeBlock) { + lines.push('```'); + } + + return lines.join('\n'); + } } export class ChatRequestViewModel implements IChatRequestViewModel { @@ -257,7 +349,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.username; } - get avatarIconUri() { + get avatarIcon() { return this._model.avatarIconUri; } @@ -271,7 +363,9 @@ export class ChatRequestViewModel implements IChatRequestViewModel { currentRenderedHeight: number | undefined; - constructor(readonly _model: IChatRequestModel) { } + constructor( + readonly _model: IChatRequestModel, + ) { } } export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel { @@ -300,8 +394,8 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.username; } - get avatarIconUri() { - return this._model.avatarIconUri; + get avatarIcon() { + return this._model.avatarIcon; } get agent() { @@ -393,7 +487,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi constructor( private readonly _model: IChatResponseModel, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, ) { super(); @@ -444,7 +538,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi export interface IChatWelcomeMessageViewModel { readonly id: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; currentRenderedHeight?: number; diff --git a/code/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/code/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts new file mode 100644 index 00000000000..092ff4adeca --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IReference } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + + +export class CodeBlockModelCollection extends Disposable { + + private readonly _models = new ResourceMap>>(); + + constructor( + @ITextModelService private readonly textModelService: ITextModelService + ) { + super(); + } + + public override dispose(): void { + super.dispose(); + this.clear(); + } + + get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): Promise> | undefined { + const uri = this.getUri(sessionId, chat, codeBlockIndex); + return this._models.get(uri); + } + + getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): Promise> { + const existing = this.get(sessionId, chat, codeBlockIndex); + if (existing) { + return existing; + } + + const uri = this.getUri(sessionId, chat, codeBlockIndex); + const ref = this.textModelService.createModelReference(uri); + this._models.set(uri, ref); + return ref; + } + + clear(): void { + this._models.forEach(async (model) => (await model).dispose()); + this._models.clear(); + } + + private getUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI { + const metadata = this.getUriMetaData(chat); + return URI.from({ + scheme: Schemas.vscodeChatCodeBlock, + authority: sessionId, + path: `/${chat.id}/${index}`, + fragment: metadata ? JSON.stringify(metadata) : undefined, + }); + } + + private getUriMetaData(chat: IChatRequestViewModel | IChatResponseViewModel) { + if (!isResponseVM(chat)) { + return undefined; + } + + return { + references: chat.contentReferences.map(ref => { + if (URI.isUri(ref.reference)) { + return { + uri: ref.reference.toJSON() + }; + } + + return { + uri: ref.reference.uri.toJSON(), + range: ref.reference.range, + }; + }) + }; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/languageModels.ts b/code/src/vs/workbench/contrib/chat/common/languageModels.ts index 7f93594aee3..7304d002628 100644 --- a/code/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/code/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -28,6 +28,7 @@ export interface IChatResponseFragment { export interface ILanguageModelChatMetadata { readonly extension: ExtensionIdentifier; + readonly identifier: string; readonly model: string; readonly description?: string; readonly auth?: { @@ -47,7 +48,7 @@ export interface ILanguageModelsService { readonly _serviceBrand: undefined; - onDidChangeLanguageModels: Event<{ added?: string[]; removed?: string[] }>; + onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>; getLanguageModelIds(): string[]; @@ -63,8 +64,8 @@ export class LanguageModelsService implements ILanguageModelsService { private readonly _providers: Map = new Map(); - private readonly _onDidChangeProviders = new Emitter<{ added?: string[]; removed?: string[] }>(); - readonly onDidChangeLanguageModels: Event<{ added?: string[]; removed?: string[] }> = this._onDidChangeProviders.event; + private readonly _onDidChangeProviders = new Emitter<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>(); + readonly onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }> = this._onDidChangeProviders.event; dispose() { this._onDidChangeProviders.dispose(); @@ -84,7 +85,7 @@ export class LanguageModelsService implements ILanguageModelsService { throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); } this._providers.set(identifier, provider); - this._onDidChangeProviders.fire({ added: [identifier] }); + this._onDidChangeProviders.fire({ added: [provider.metadata] }); return toDisposable(() => { if (this._providers.delete(identifier)) { this._onDidChangeProviders.fire({ removed: [identifier] }); diff --git a/code/src/vs/workbench/contrib/chat/common/voiceChat.ts b/code/src/vs/workbench/contrib/chat/common/voiceChat.ts index 9c92a44354a..1c93007b8cf 100644 --- a/code/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/code/src/vs/workbench/contrib/chat/common/voiceChat.ts @@ -30,7 +30,7 @@ export interface IVoiceChatService { * if the user says "at workspace slash fix this problem", the result * will be "@workspace /fix this problem". */ - createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): IVoiceChatSession; + createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise; } export interface IVoiceChatTextEvent extends ISpeechToTextEvent { @@ -87,19 +87,16 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private createPhrases(model?: IChatModel): Map { const phrases = new Map(); - for (const agent of this.chatAgentService.getAgents()) { + for (const agent of this.chatAgentService.getActivatedAgents()) { const agentPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]} ${VoiceChatService.CHAT_AGENT_ALIAS.get(agent.id) ?? agent.id}`.toLowerCase(); phrases.set(agentPhrase, { agent: agent.id }); - const commands = model && agent.getLastSlashCommands(model); - if (commands) { - for (const slashCommand of commands) { - const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); - phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); + for (const slashCommand of agent.slashCommands) { + const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); + phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); - phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - } + const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); + phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); } } @@ -117,7 +114,7 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { } } - createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): IVoiceChatSession { + async createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise { const disposables = new DisposableStore(); disposables.add(token.onCancellationRequested(() => disposables.dispose())); @@ -125,7 +122,7 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { let detectedSlashCommand = false; const emitter = disposables.add(new Emitter()); - const session = this.speechService.createSpeechToTextSession(token, 'chat'); + const session = await this.speechService.createSpeechToTextSession(token, 'chat'); const phrases = this.createPhrases(options.model); disposables.add(session.onDidChange(e => { diff --git a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css index 7a43cef7d12..3411d749fce 100644 --- a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css +++ b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -7,15 +7,18 @@ * Replace with "microphone" icon. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, -.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { +.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, +.monaco-workbench .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: "\ec1c"; + font-family: 'codicon'; } /* * Clear animation styles when reduced motion is enabled. */ .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), -.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), +.monaco-workbench.reduce-motion .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { animation: none; } @@ -23,6 +26,8 @@ * Replace with "stop" icon when reduced motion is enabled. */ .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, -.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, +.monaco-workbench.reduce-motion .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: "\ead7"; + font-family: 'codicon'; } diff --git a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index b508d24ff6b..3da3d090877 100644 --- a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/voiceChatActions'; import { Event } from 'vs/base/common/event'; import { firstOrDefault } from 'vs/base/common/arrays'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize, localize2 } from 'vs/nls'; @@ -46,26 +46,28 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IHostService } from 'vs/workbench/services/host/browser/host'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ThemeIcon } from 'vs/base/common/themables'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") }); const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") }); const CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS = new RawContextKey('quickVoiceChatInProgress', false, { type: 'boolean', description: localize('quickVoiceChatInProgress', "True when voice recording from microphone is in progress for quick chat.") }); const CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS = new RawContextKey('inlineVoiceChatInProgress', false, { type: 'boolean', description: localize('inlineVoiceChatInProgress', "True when voice recording from microphone is in progress for inline chat.") }); +const CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS = new RawContextKey('terminalVoiceChatInProgress', false, { type: 'boolean', description: localize('terminalVoiceChatInProgress', "True when voice recording from microphone is in progress for terminal chat.") }); const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voiceChatInViewInProgress', false, { type: 'boolean', description: localize('voiceChatInViewInProgress', "True when voice recording from microphone is in progress in the chat view.") }); const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); const CanVoiceChat = ContextKeyExpr.and(CONTEXT_PROVIDER_EXISTS, HasSpeechProvider); const FocusInChatInput = assertIsDefined(ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT)); -type VoiceChatSessionContext = 'inline' | 'quick' | 'view' | 'editor'; +type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; interface IVoiceChatSessionController { @@ -89,14 +91,16 @@ class VoiceChatSessionControllerFactory { static create(accessor: ServicesAccessor, context: 'quick'): Promise; static create(accessor: ServicesAccessor, context: 'view'): Promise; static create(accessor: ServicesAccessor, context: 'focused'): Promise; - static create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focused'): Promise; - static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focused'): Promise { + static create(accessor: ServicesAccessor, context: 'terminal'): Promise; + static create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise; + static async create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise { const chatWidgetService = accessor.get(IChatWidgetService); const viewsService = accessor.get(IViewsService); const chatContributionService = accessor.get(IChatContributionService); const quickChatService = accessor.get(IQuickChatService); const layoutService = accessor.get(IWorkbenchLayoutService); const editorService = accessor.get(IEditorService); + const terminalService = accessor.get(ITerminalService); // Currently Focused Context if (context === 'focused') { @@ -131,6 +135,15 @@ class VoiceChatSessionControllerFactory { return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); } } + + // Try with the terminal chat + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); + } + } } // View Chat @@ -152,6 +165,17 @@ class VoiceChatSessionControllerFactory { } } + // Terminal Chat + if (context === 'terminal') { + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat) { + return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); + } + } + } + // Quick Chat if (context === 'quick') { quickChatService.open(); @@ -232,6 +256,20 @@ class VoiceChatSessionControllerFactory { clearInputPlaceholder: () => inlineChat.resetPlaceholder() }; } + + private static doCreateForTerminalChat(terminalChat: TerminalChatController): IVoiceChatSessionController { + return { + context: 'terminal', + onDidAcceptInput: terminalChat.onDidAcceptInput, + onDidCancelInput: terminalChat.onDidCancelInput, + focusInput: () => terminalChat.focus(), + acceptInput: () => terminalChat.acceptInput(), + updateInput: text => terminalChat.updateInput(text, false), + getInput: () => terminalChat.getInput(), + setInputPlaceholder: text => terminalChat.setPlaceholder(text), + clearInputPlaceholder: () => terminalChat.resetPlaceholder() + }; + } } interface IVoiceChatSession { @@ -263,6 +301,7 @@ class VoiceChatSessions { private quickVoiceChatInProgressKey = CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); private inlineVoiceChatInProgressKey = CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); + private terminalVoiceChatInProgressKey = CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); private voiceChatInViewInProgressKey = CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.bindTo(this.contextKeyService); private voiceChatInEditorInProgressKey = CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.bindTo(this.contextKeyService); @@ -275,7 +314,7 @@ class VoiceChatSessions { @IConfigurationService private readonly configurationService: IConfigurationService ) { } - start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): IVoiceChatSession { + async start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): Promise { this.stop(); let disableTimeout = false; @@ -300,7 +339,7 @@ class VoiceChatSessions { this.voiceChatGettingReadyKey.set(true); - const voiceChatSession = this.voiceChatService.createVoiceChatSession(cts.token, { usesAgents: controller.context !== 'inline', model: context?.widget?.viewModel?.model }); + const voiceChatSession = await this.voiceChatService.createVoiceChatSession(cts.token, { usesAgents: controller.context !== 'inline', model: context?.widget?.viewModel?.model }); let inputValue = controller.getInput(); @@ -353,6 +392,9 @@ class VoiceChatSessions { case 'inline': this.inlineVoiceChatInProgressKey.set(true); break; + case 'terminal': + this.terminalVoiceChatInProgressKey.set(true); + break; case 'quick': this.quickVoiceChatInProgressKey.set(true); break; @@ -395,6 +437,7 @@ class VoiceChatSessions { this.quickVoiceChatInProgressKey.set(false); this.inlineVoiceChatInProgressKey.set(false); + this.terminalVoiceChatInProgressKey.set(false); this.voiceChatInViewInProgressKey.set(false); this.voiceChatInEditorInProgressKey.set(false); } @@ -419,10 +462,12 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor const holdMode = keybindingService.enableKeybindingHoldMode(id); + let session: IVoiceChatSession | undefined = undefined; + let acceptVoice = false; const handle = disposableTimeout(() => { acceptVoice = true; - session.setTimeoutDisabled(true); // disable accept on timeout when hold mode runs for VOICE_KEY_HOLD_THRESHOLD + session?.setTimeoutDisabled(true); // disable accept on timeout when hold mode runs for VOICE_KEY_HOLD_THRESHOLD }, VOICE_KEY_HOLD_THRESHOLD); const controller = await VoiceChatSessionControllerFactory.create(accessor, target); @@ -431,7 +476,7 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor return; } - const session = VoiceChatSessions.getInstance(instantiationService).start(controller, context); + session = await VoiceChatSessions.getInstance(instantiationService).start(controller, context); await holdMode; handle.dispose(); @@ -502,7 +547,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { const handle = disposableTimeout(async () => { const controller = await VoiceChatSessionControllerFactory.create(accessor, 'view'); if (controller) { - session = VoiceChatSessions.getInstance(instantiationService).start(controller, context); + session = await VoiceChatSessions.getInstance(instantiationService).start(controller, context); session.setTimeoutDisabled(true); } }, VOICE_KEY_HOLD_THRESHOLD); @@ -566,7 +611,8 @@ export class StartVoiceChatAction extends Action2 { CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), - CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate() + CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate(), + CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate() ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, @@ -582,6 +628,12 @@ export class StartVoiceChatAction extends Action2 { when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), group: 'main', order: -1 + }, + { + id: MenuId.for('terminalChatInput'), + when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate()), + group: 'main', + order: -1 }] }); } @@ -628,42 +680,24 @@ export class InstallVoiceChatAction extends Action2 { when: HasSpeechProvider.negate(), group: 'main', order: -1 + }, { + id: MenuId.for('terminalChatInput'), + when: HasSpeechProvider.negate(), + group: 'main', + order: -1 }] }); } async run(accessor: ServicesAccessor): Promise { const contextKeyService = accessor.get(IContextKeyService); - const dialogService = accessor.get(IDialogService); - const extensionManagementService = accessor.get(IExtensionsWorkbenchService); - - const extension = firstOrDefault((await extensionManagementService.getExtensions([{ id: InstallVoiceChatAction.SPEECH_EXTENSION_ID }], CancellationToken.None))); - if (!extension) { - return; - } - - if (extension.state === ExtensionState.Installed) { - await dialogService.info( - localize('enableExtensionMessage', "Microphone support requires an extension. Please enable it."), - localize('enableExtensionDetail', "Extension '{0}' is currently disabled.", InstallVoiceChatAction.SPEECH_EXTENSION_ID), - ); - - return extensionManagementService.open(extension); - } - - const { confirmed } = await dialogService.confirm({ - message: localize('confirmInstallMessage', "Microphone support requires an extension. Would you like to install it now?"), - detail: localize('confirmInstallDetail', "This will install the '{0}' extension.", InstallVoiceChatAction.SPEECH_EXTENSION_ID), - primaryButton: localize({ key: 'installButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Install") - }); - - if (!confirmed) { - return; - } - + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); try { InstallingSpeechProvider.bindTo(contextKeyService).set(true); - await extensionManagementService.install(extension, undefined, ProgressLocation.Notification); + await extensionsWorkbenchService.install(InstallVoiceChatAction.SPEECH_EXTENSION_ID, { + justification: localize('confirmInstallDetail', "Microphone support requires this extension."), + enable: true + }, ProgressLocation.Notification); } finally { InstallingSpeechProvider.bindTo(contextKeyService).set(false); } @@ -674,7 +708,7 @@ class BaseStopListeningAction extends Action2 { constructor( desc: { id: string; icon?: ThemeIcon; f1?: boolean }, - private readonly target: 'inline' | 'quick' | 'view' | 'editor' | undefined, + private readonly target: 'inline' | 'terminal' | 'quick' | 'view' | 'editor' | undefined, context: RawContextKey, menu: MenuId | undefined, group: 'navigation' | 'main' = 'navigation' @@ -747,6 +781,15 @@ export class StopListeningInInlineChatAction extends BaseStopListeningAction { } } +export class StopListeningInTerminalChatAction extends BaseStopListeningAction { + + static readonly ID = 'workbench.action.chat.stopListeningInTerminalChat'; + + constructor() { + super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, 'terminal', CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS, MenuId.for('terminalChatInput'), 'main'); + } +} + export class StopListeningAndSubmitAction extends Action2 { static readonly ID = 'workbench.action.chat.stopListeningAndSubmit'; @@ -785,10 +828,42 @@ registerThemingParticipant((theme, collector) => { // Show a "microphone" icon when recording is in progress that glows via outline. collector.addRule(` .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), - .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + color: ${activeRecordingColor}; + outline: 1px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1s infinite; + border-radius: 50%; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; border-radius: 50%; + width: 16px; + height: 16px; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after, + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { outline: 2px solid ${activeRecordingColor}; - animation: pulseAnimation 1500ms ease-in-out infinite !important; + outline-offset: -1px; + animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; } @keyframes pulseAnimation { @@ -848,7 +923,7 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe } private registerListeners(): void { - this._register(Event.runAndSubscribe(this.speechService.onDidRegisterSpeechProvider, () => { + this._register(Event.runAndSubscribe(this.speechService.onDidChangeHasSpeechProvider, () => { this.updateConfiguration(); this.handleKeywordActivation(); })); @@ -993,7 +1068,7 @@ class KeywordActivationStatusEntry extends Disposable { ) { super(); - CommandsRegistry.registerCommand(KeywordActivationStatusEntry.STATUS_COMMAND, () => this.commandService.executeCommand('workbench.action.openSettings', KEYWORD_ACTIVIATION_SETTING_ID)); + this._register(CommandsRegistry.registerCommand(KeywordActivationStatusEntry.STATUS_COMMAND, () => this.commandService.executeCommand('workbench.action.openSettings', KEYWORD_ACTIVIATION_SETTING_ID))); this.registerListeners(); this.updateStatusEntry(); diff --git a/code/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/code/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 08b145350e1..eb473ee8091 100644 --- a/code/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/code/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -22,5 +22,6 @@ registerAction2(StopListeningInChatViewAction); registerAction2(StopListeningInChatEditorAction); registerAction2(StopListeningInQuickChatAction); registerAction2(StopListeningInInlineChatAction); +registerAction2(StopListeningInTerminalChatAction); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index 4a241114279..ae1818689d0 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index becd9bf6f31..190c3555054 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 50c67ea58d0..65d00490273 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index 345e8c874de..6585ff1e0e5 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index 406e20cfe55..fc2a622c9ac 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 31fd0b94e96..6f9eaa531cf 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index 85bc82a3136..fee5731f3e3 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap index d58da8b3744..ee94d5b3c93 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap @@ -23,7 +23,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,10 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ], + locations: [ 1 ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap index 0939983222f..75c5fa71f40 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap @@ -1,7 +1,7 @@ { - requesterUsername: "", + requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "", + responderUsername: "test", responderAvatarIconUri: undefined, welcomeMessage: undefined, requests: [ ], diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap index 3a6b248a792..dfc16b58de3 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap @@ -22,7 +22,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,10 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ], + locations: [ 1 ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index ba397535bff..b1d4231aeb2 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ChatAgentService, IChatAgent, IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentService, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -112,7 +112,7 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return >{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => [], getLastSlashCommands: () => slashCommands }; + return { id: 'agent', metadata: { description: '' }, slashCommands }; }; test('agent with subcommand after text', async () => { diff --git a/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 3a2b6abc998..64111ef5f30 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -21,7 +21,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { ChatAgentService, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -32,6 +32,7 @@ import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/ import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; class SimpleTestProvider extends Disposable implements IChatProvider { private static sessionId = 0; @@ -45,8 +46,6 @@ class SimpleTestProvider extends Disposable implements IChatProvider { async prepareSession(): Promise { return { id: SimpleTestProvider.sessionId++, - responderUsername: 'test', - requesterUsername: 'test', }; } @@ -59,13 +58,9 @@ const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContext: IChatAgent = { id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, + locations: [ChatAgentLocation.Panel], metadata: {}, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands() { - return []; - }, + slashCommands: [], async invoke(request, progress, history, token) { progress({ documents: [ @@ -109,25 +104,22 @@ suite('Chat', () => { instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IChatContributionService, new MockChatContributionService( + [ + { extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }, + { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId }, + ])); chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); instantiationService.stub(IChatAgentService, chatAgentService); const agent = { - id: 'testAgent', - extensionId: nullExtensionDescription.identifier, - metadata: { isDefault: true }, async invoke(request, progress, history, token) { return {}; }, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands(token) { - return []; - }, - } as IChatAgent; - testDisposables.add(chatAgentService.registerAgent(agent)); + } satisfies IChatAgentImplementation; + testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); }); test('retrieveSession', async () => { @@ -203,18 +195,6 @@ suite('Chat', () => { }, 'Expected to throw for dupe provider'); }); - test('sendRequestToProvider', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); - - const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None)); - assert.strictEqual(model.getRequests().length, 0); - - const response = await testService.sendRequestToProvider(model.sessionId, { message: 'test request' }); - await response?.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 1); - }); - test('addCompleteRequest', async () => { const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); @@ -229,7 +209,8 @@ suite('Chat', () => { }); test('can serialize', async () => { - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); + chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); @@ -249,7 +230,7 @@ suite('Chat', () => { test('can deserialize', async () => { let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); // create the first service, send request, get response, and serialize the state { // serapate block to not leak variables in outer scope diff --git a/code/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/code/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts new file mode 100644 index 00000000000..e1adddb2dec --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; + +export class MockChatContributionService implements IChatContributionService { + _serviceBrand: undefined; + + constructor( + public readonly registeredParticipants: IChatParticipantContribution[] = [] + ) { } + + registeredProviders: IChatProviderContribution[] = []; + registerChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + + registerChatProvider(provider: IChatProviderContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatProvider(providerId: string): void { + throw new Error('Method not implemented.'); + } + getViewIdForProvider(providerId: string): string { + throw new Error('Method not implemented.'); + } +} diff --git a/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 35c17753392..d961bbb74c5 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatCompleteResponse, IChatDetail, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; @@ -60,9 +60,6 @@ export class MockChatService implements IChatService { addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void { throw new Error('Method not implemented.'); } - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void { - throw new Error('Method not implemented.'); - } getHistory(): IChatDetail[] { throw new Error('Method not implemented.'); } diff --git a/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index 4e34de6d72a..38c63ebeee0 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; export class MockChatVariablesService implements IChatVariablesService { _serviceBrand: undefined; @@ -32,4 +32,8 @@ export class MockChatVariablesService implements IChatVariablesService { variables: [] }; } + + resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/code/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/code/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index c85e0056d12..c993bae97ea 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -11,7 +11,7 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IChatAgent, IChatAgentCommand, IChatAgentHistoryEntry, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; @@ -27,11 +27,9 @@ suite('VoiceChat', () => { class TestChatAgent implements IChatAgent { extensionId: ExtensionIdentifier = nullExtensionDescription.identifier; - - constructor(readonly id: string, private readonly lastSlashCommands: IChatAgentCommand[]) { } - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined { return this.lastSlashCommands; } + locations: ChatAgentLocation[] = [ChatAgentLocation.Panel]; + constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } metadata = {}; } @@ -49,22 +47,23 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; - registerAgent(agent: IChatAgent): IDisposable { throw new Error(); } + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { throw new Error(); } - getAgents(): Array { return agents; } + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } + getRegisteredAgents(): Array { return agents; } + getActivatedAgents(): IChatAgent[] { return agents; } + getAgents(): IChatAgent[] { return agents; } getAgent(id: string): IChatAgent | undefined { throw new Error(); } getDefaultAgent(): IChatAgent | undefined { throw new Error(); } getSecondaryAgent(): IChatAgent | undefined { throw new Error(); } - hasAgent(id: string): boolean { throw new Error(); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error(); } } class TestSpeechService implements ISpeechService { _serviceBrand: undefined; - onDidRegisterSpeechProvider = Event.None; - onDidUnregisterSpeechProvider = Event.None; + onDidChangeHasSpeechProvider = Event.None; readonly hasSpeechProvider = true; readonly hasActiveSpeechToTextSession = false; @@ -74,7 +73,7 @@ suite('VoiceChat', () => { onDidStartSpeechToTextSession = Event.None; onDidEndSpeechToTextSession = Event.None; - createSpeechToTextSession(token: CancellationToken): ISpeechToTextSession { + async createSpeechToTextSession(token: CancellationToken): Promise { return { onDidChange: emitter.event }; @@ -91,10 +90,10 @@ suite('VoiceChat', () => { let service: VoiceChatService; let event: IVoiceChatTextEvent | undefined; - function createSession(options: IVoiceChatSessionOptions) { + async function createSession(options: IVoiceChatSessionOptions) { const cts = new CancellationTokenSource(); disposables.add(toDisposable(() => cts.dispose(true))); - const session = service.createVoiceChatSession(cts.token, options); + const session = await service.createVoiceChatSession(cts.token, options); disposables.add(session.onDidChange(e => { event = e; })); @@ -110,17 +109,17 @@ suite('VoiceChat', () => { }); test('Agent and slash command detection (useAgents: false)', async () => { - testAgentsAndSlashCommandsDetection({ usesAgents: false, model: {} as IChatModel }); + await testAgentsAndSlashCommandsDetection({ usesAgents: false, model: {} as IChatModel }); }); test('Agent and slash command detection (useAgents: true)', async () => { - testAgentsAndSlashCommandsDetection({ usesAgents: true, model: {} as IChatModel }); + await testAgentsAndSlashCommandsDetection({ usesAgents: true, model: {} as IChatModel }); }); - function testAgentsAndSlashCommandsDetection(options: IVoiceChatSessionOptions) { + async function testAgentsAndSlashCommandsDetection(options: IVoiceChatSessionOptions) { // Nothing to detect - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Started }); assert.strictEqual(event?.status, SpeechToTextStatus.Started); @@ -141,7 +140,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, undefined); // Agent - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -168,7 +167,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Agent with punctuation - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -180,7 +179,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help'); assert.strictEqual(event?.waitingForInput, false); - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At Workspace. help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -193,7 +192,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Slash Command - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Slash fix' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -206,7 +205,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, true); // Agent + Slash Command - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code slash search help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -219,7 +218,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Agent + Slash Command with punctuation - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code, slash search, help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -231,7 +230,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help'); assert.strictEqual(event?.waitingForInput, false); - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code. slash, search help' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -244,7 +243,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.waitingForInput, false); // Agent not detected twice - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, for at workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -258,7 +257,7 @@ suite('VoiceChat', () => { // Slash command detected after agent recognized if (options.usesAgents) { - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognized); @@ -280,7 +279,7 @@ suite('VoiceChat', () => { assert.strictEqual(event?.text, '/fix'); assert.strictEqual(event?.waitingForInput, true); - createSession(options); + await createSession(options); emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognized); @@ -297,7 +296,7 @@ suite('VoiceChat', () => { test('waiting for input', async () => { // Agent - createSession({ usesAgents: true, model: {} as IChatModel }); + await createSession({ usesAgents: true, model: {} as IChatModel }); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); @@ -310,7 +309,7 @@ suite('VoiceChat', () => { assert.strictEqual(event.waitingForInput, true); // Slash Command - createSession({ usesAgents: true, model: {} as IChatModel }); + await createSession({ usesAgents: true, model: {} as IChatModel }); emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace slash explain' }); assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing); diff --git a/code/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/code/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index 4225ffa7cd7..38fd5502ae2 100644 --- a/code/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/code/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; @@ -103,11 +104,11 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon } private getSourceActions(contributions: readonly CodeActionsExtensionPoint[]) { - const defaultKinds = Object.keys(codeActionsOnSaveDefaultProperties).map(value => new CodeActionKind(value)); + const defaultKinds = Object.keys(codeActionsOnSaveDefaultProperties).map(value => new HierarchicalKind(value)); const sourceActions = new Map(); for (const contribution of contributions) { for (const action of contribution.actions) { - const kind = new CodeActionKind(action.kind); + const kind = new HierarchicalKind(action.kind); if (CodeActionKind.Source.contains(kind) // Exclude any we already included by default && !defaultKinds.some(defaultKind => defaultKind.contains(kind)) @@ -149,12 +150,12 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon }; }; - const getActions = (ofKind: CodeActionKind): ContributedCodeAction[] => { + const getActions = (ofKind: HierarchicalKind): ContributedCodeAction[] => { const allActions = this._contributedCodeActions.flatMap(desc => desc.actions); const out = new Map(); for (const action of allActions) { - if (!out.has(action.kind) && ofKind.contains(new CodeActionKind(action.kind))) { + if (!out.has(action.kind) && ofKind.contains(new HierarchicalKind(action.kind))) { out.set(action.kind, action); } } @@ -162,7 +163,7 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon }; return [ - conditionalSchema(codeActionCommandId, getActions(CodeActionKind.Empty)), + conditionalSchema(codeActionCommandId, getActions(HierarchicalKind.Empty)), conditionalSchema(refactorCommandId, getActions(CodeActionKind.Refactor)), conditionalSchema(sourceActionCommandId, getActions(CodeActionKind.Source)), ]; diff --git a/code/src/vs/workbench/contrib/codeActions/browser/documentationContribution.ts b/code/src/vs/workbench/contrib/codeActions/browser/documentationContribution.ts index ed964662b2e..01f9be18e30 100644 --- a/code/src/vs/workbench/contrib/codeActions/browser/documentationContribution.ts +++ b/code/src/vs/workbench/contrib/codeActions/browser/documentationContribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -69,7 +70,7 @@ export class CodeActionDocumentationContribution extends Disposable implements I public _getAdditionalMenuItems(context: languages.CodeActionContext, actions: readonly languages.CodeAction[]): languages.Command[] { if (context.only !== CodeActionKind.Refactor.value) { - if (!actions.some(action => action.kind && CodeActionKind.Refactor.contains(new CodeActionKind(action.kind)))) { + if (!actions.some(action => action.kind && CodeActionKind.Refactor.contains(new HierarchicalKind(action.kind)))) { return []; } } diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/code/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index 842953efd0d..91727c3a14c 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts @@ -193,7 +193,7 @@ export class EditorDictation extends Disposable implements IEditorContribution { super(); } - start() { + async start(): Promise { const disposables = new DisposableStore(); this.sessionDisposables.value = disposables; @@ -206,6 +206,8 @@ export class EditorDictation extends Disposable implements IEditorContribution { const collection = this.editor.createDecorationsCollection(); disposables.add(toDisposable(() => collection.clear())); + disposables.add(this.editor.onDidChangeCursorPosition(() => this.widget.layout())); + let previewStart: Position | undefined = undefined; let lastReplaceTextLength = 0; @@ -242,13 +244,12 @@ export class EditorDictation extends Disposable implements IEditorContribution { } this.editor.revealPositionInCenterIfOutsideViewport(endPosition); - this.widget.layout(); }; const cts = new CancellationTokenSource(); disposables.add(toDisposable(() => cts.dispose(true))); - const session = this.speechService.createSpeechToTextSession(cts.token); + const session = await this.speechService.createSpeechToTextSession(cts.token); disposables.add(session.onDidChange(e => { if (cts.token.isCancellationRequested) { return; diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index a19d88cca33..b47c9bc5642 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -8,7 +8,7 @@ import { autorunWithStore, observableFromEvent } from 'vs/base/common/observable import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/diffEditor.contribution'; +import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; @@ -91,9 +91,6 @@ function createScreenReaderHelp(): IDisposable { const keybindingService = accessor.get(IKeybindingService); const contextKeyService = accessor.get(IContextKeyService); - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { return; } @@ -103,11 +100,25 @@ function createScreenReaderHelp(): IDisposable { return; } + const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); + const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); + let switchSides; + const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); + if (switchSidesKb) { + switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); + } else { + switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); + } + + const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); + const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; const content = [ localize('msg1', "You are in a diff editor."), localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), - localize('msg3', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), + switchSides, + diffEditorActiveAnnouncement, + localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), ]; const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); if (commentCommandInfo) { diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index fb540a708d1..ab46e943663 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -34,6 +34,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common/output'; import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = dom.$; @@ -224,7 +226,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { id: 'inlineChat.hintAction', from: 'hint' }); - void this.commandService.executeCommand(inlineChatId, { from: 'hint' }); + this.commandService.executeCommand(inlineChatId, { from: 'hint' }); }; const hintHandler: IContentActionHandler = { @@ -252,7 +254,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { const hintPart = $('a', undefined, fragment); hintPart.style.fontStyle = 'italic'; hintPart.style.cursor = 'pointer'; - hintPart.onclick = handleClick; + this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); return hintPart; } else { const hintPart = $('span', undefined, fragment); @@ -263,14 +265,14 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { hintElement.appendChild(before); - const label = new KeybindingLabel(hintElement, OS); + const label = hintHandler.disposables.add(new KeybindingLabel(hintElement, OS)); label.set(keybindingHint); label.element.style.width = 'min-content'; label.element.style.display = 'inline'; if (this.options.clickable) { label.element.style.cursor = 'pointer'; - label.element.onclick = handleClick; + this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); } hintElement.appendChild(after); @@ -382,7 +384,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { anchor.style.cursor = 'pointer'; const id = keybindingsLookup.shift(); const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel(); - anchor.title = title ?? ''; + hintHandler.disposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); } return { hintElement, ariaLabel }; diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index 557d9a025ec..dc43b9557b7 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -219,7 +219,7 @@ class DocumentSymbolsOutline implements IOutline { return this._outlineModel?.uri; } - async reveal(entry: DocumentSymbolItem, options: IEditorOptions, sideBySide: boolean): Promise { + async reveal(entry: DocumentSymbolItem, options: IEditorOptions, sideBySide: boolean, select: boolean): Promise { const model = OutlineModel.get(entry); if (!model || !(entry instanceof OutlineElement)) { return; @@ -228,7 +228,7 @@ class DocumentSymbolsOutline implements IOutline { resource: model.uri, options: { ...options, - selection: Range.collapseToStart(entry.symbol.selectionRange), + selection: select ? entry.symbol.range : Range.collapseToStart(entry.symbol.selectionRange), selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport, } }, this._editor, sideBySide); diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts b/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts index 4e8761ffb1d..3d9af339abe 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts @@ -24,9 +24,6 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IOutlineComparator, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; import { ThemeIcon } from 'vs/base/common/themables'; import { mainWindow } from 'vs/base/browser/window'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { nativeHoverDelegate } from 'vs/platform/hover/browser/hover'; export type DocumentSymbolItem = OutlineGroup | OutlineElement; @@ -69,6 +66,10 @@ class DocumentSymbolGroupTemplate { readonly labelContainer: HTMLElement, readonly label: HighlightedLabel, ) { } + + dispose() { + this.label.dispose(); + } } class DocumentSymbolTemplate { @@ -110,7 +111,7 @@ export class DocumentSymbolGroupRenderer implements ITreeRenderer('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageId(), resource: model.resource }); + const trimInRegexAndStrings = this.configurationService.getValue('files.trimTrailingWhitespaceInRegexAndStrings', { overrideIdentifier: model.textEditorModel.getLanguageId(), resource: model.resource }); + if (trimTrailingWhitespaceOption) { + this.doTrimTrailingWhitespace(model.textEditorModel, context.reason === SaveReason.AUTO, trimInRegexAndStrings); } } - private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void { + private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean, trimInRegexesAndStrings: boolean): void { let prevSelection: Selection[] = []; let cursors: Position[] = []; @@ -71,7 +74,7 @@ export class TrimWhitespaceParticipant implements ITextFileSaveParticipant { } } - const ops = trimTrailingWhitespace(model, cursors); + const ops = trimTrailingWhitespace(model, cursors, trimInRegexesAndStrings); if (!ops.length) { return; // Nothing to do } @@ -322,7 +325,7 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { ? [] : Object.keys(setting) .filter(x => setting[x] === 'never' || false) - .map(x => new CodeActionKind(x)); + .map(x => new HierarchicalKind(x)); progress.report({ message: localize('codeaction', "Quick Fixes") }); @@ -331,8 +334,8 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { await this.applyOnSaveActions(textEditorModel, filteredSaveList, excludedActions, progress, token); } - private createCodeActionsOnSave(settingItems: readonly string[]): CodeActionKind[] { - const kinds = settingItems.map(x => new CodeActionKind(x)); + private createCodeActionsOnSave(settingItems: readonly string[]): HierarchicalKind[] { + const kinds = settingItems.map(x => new HierarchicalKind(x)); // Remove subsets return kinds.filter(kind => { @@ -340,7 +343,7 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { }); } - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken): Promise { + private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly HierarchicalKind[], excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken): Promise { const getActionProgress = new class implements IProgress { private _names = new Set(); @@ -385,7 +388,7 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { } } - private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken) { + private getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken) { return getCodeActions(this.languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.OnSave, diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/code/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 60e13398790..6fbc04ff214 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -30,7 +30,7 @@ import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreve import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { HistoryNavigator } from 'vs/base/common/history'; import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; diff --git a/code/src/vs/workbench/contrib/comments/browser/commentNode.ts b/code/src/vs/workbench/contrib/comments/browser/commentNode.ts index 58e376f9561..df2afb078e5 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -9,7 +9,7 @@ import * as languages from 'vs/editor/common/languages'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IActionRunner, IAction, Separator, ActionRunner } from 'vs/base/common/actions'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -50,6 +50,7 @@ import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/c import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; class CommentsActionRunner extends ActionRunner { protected override async runAction(action: IAction, context: any[]): Promise { @@ -60,6 +61,7 @@ class CommentsActionRunner extends ActionRunner { export class CommentNode extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; + private _avatar: HTMLElement; private _md: HTMLElement | undefined; private _plainText: HTMLElement | undefined; private _clearTimeout: any; @@ -129,12 +131,9 @@ export class CommentNode extends Disposable { this._commentMenus = this.commentService.getCommentMenus(this.owner); this._domNode.tabIndex = -1; - const avatar = dom.append(this._domNode, dom.$('div.avatar-container')); - if (comment.userIconPath) { - const img = dom.append(avatar, dom.$('img.avatar')); - img.src = FileAccess.uriToBrowserUri(URI.revive(comment.userIconPath)).toString(true); - img.onerror = _ => img.remove(); - } + this._avatar = dom.append(this._domNode, dom.$('div.avatar-container')); + this.updateCommentUserIcon(this.comment.userIconPath); + this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); this.createHeader(this._commentDetailsContainer); @@ -223,6 +222,15 @@ export class CommentNode extends Disposable { } } + private updateCommentUserIcon(userIconPath: UriComponents | undefined) { + this._avatar.textContent = ''; + if (userIconPath) { + const img = dom.append(this._avatar, dom.$('img.avatar')); + img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true); + img.onerror = _ => img.remove(); + } + } + public get onDidClick(): Event> { return this._onDidClick.event; } @@ -286,7 +294,7 @@ export class CommentNode extends Disposable { return result; } - private get commentNodeContext() { + private get commentNodeContext(): [any, MarshalledCommentThread] { return [{ thread: this.commentThread, commentUniqueId: this.comment.uniqueIdInThread, @@ -701,6 +709,10 @@ export class CommentNode extends Disposable { this.updateCommentBody(newComment.body); } + if (this.comment.userIconPath && newComment.userIconPath && (URI.from(this.comment.userIconPath).toString() !== URI.from(newComment.userIconPath).toString())) { + this.updateCommentUserIcon(newComment.userIconPath); + } + const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode; this.comment = newComment; diff --git a/code/src/vs/workbench/contrib/comments/browser/commentReply.ts b/code/src/vs/workbench/contrib/comments/browser/commentReply.ts index 4d2a9e4b887..55ea0d4e8f9 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -30,8 +30,8 @@ import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/comme import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { LayoutableEditor, MIN_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from './simpleCommentEditor'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const COMMENT_SCHEME = 'comment'; let INMEM_MODEL_ID = 0; diff --git a/code/src/vs/workbench/contrib/comments/browser/commentService.ts b/code/src/vs/workbench/contrib/comments/browser/commentService.ts index 1072a60aedf..accc000bdce 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread, CommentOptions, PendingCommentThread } from 'vs/editor/common/languages'; +import { CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread, CommentOptions, PendingCommentThread, CommentingRangeResourceHint } from 'vs/editor/common/languages'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -21,6 +21,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { ILogService } from 'vs/platform/log/common/log'; import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel'; +import { IModelService } from 'vs/editor/common/services/model'; export const ICommentService = createDecorator('commentService'); @@ -30,14 +31,14 @@ interface IResourceCommentThreadEvent { } export interface ICommentInfo extends CommentInfo { - owner: string; + uniqueOwner: string; label?: string; } export interface INotebookCommentInfo { extensionId?: string; threads: CommentThread[]; - owner: string; + uniqueOwner: string; label?: string; } @@ -48,7 +49,7 @@ export interface IWorkspaceCommentThreadsEvent { } export interface INotebookCommentThreadChangedEvent extends CommentThreadChangedEvent { - owner: string; + uniqueOwner: string; } export interface ICommentController { @@ -61,6 +62,7 @@ export interface ICommentController { }; options?: CommentOptions; contextValue?: string; + owner: string; createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise; updateCommentThreadTemplate(threadHandle: number, range: IRange): Promise; deleteCommentThreadMain(commentThreadId: string): void; @@ -82,7 +84,7 @@ export interface ICommentService { readonly onDidUpdateNotebookCommentThreads: Event; readonly onDidChangeActiveEditingCommentThread: Event; readonly onDidChangeCurrentCommentThread: Event; - readonly onDidUpdateCommentingRanges: Event<{ owner: string }>; + readonly onDidUpdateCommentingRanges: Event<{ uniqueOwner: string }>; readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; @@ -90,28 +92,29 @@ export interface ICommentService { readonly isCommentingEnabled: boolean; readonly commentsModel: ICommentsModel; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void; - removeWorkspaceComments(owner: string): void; - registerCommentController(owner: string, commentControl: ICommentController): void; - unregisterCommentController(owner?: string): void; - getCommentController(owner: string): ICommentController | undefined; - createCommentThreadTemplate(owner: string, resource: URI, range: Range | undefined): Promise; - updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise; - getCommentMenus(owner: string): CommentMenus; + setWorkspaceComments(uniqueOwner: string, commentsByResource: CommentThread[]): void; + removeWorkspaceComments(uniqueOwner: string): void; + registerCommentController(uniqueOwner: string, commentControl: ICommentController): void; + unregisterCommentController(uniqueOwner?: string): void; + getCommentController(uniqueOwner: string): ICommentController | undefined; + createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise; + updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range): Promise; + getCommentMenus(uniqueOwner: string): CommentMenus; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; updateNotebookComments(ownerId: string, event: CommentThreadChangedEvent): void; disposeCommentThread(ownerId: string, threadId: string): void; getDocumentComments(resource: URI): Promise<(ICommentInfo | null)[]>; getNotebookComments(resource: URI): Promise<(INotebookCommentInfo | null)[]>; - updateCommentingRanges(ownerId: string): void; - hasReactionHandler(owner: string): boolean; - toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; + updateCommentingRanges(ownerId: string, resourceHints?: CommentingRangeResourceHint): void; + hasReactionHandler(uniqueOwner: string): boolean; + toggleReaction(uniqueOwner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; setActiveEditingCommentThread(commentThread: CommentThread | null): void; setCurrentCommentThread(commentThread: CommentThread | undefined): void; - setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; + setActiveCommentAndThread(uniqueOwner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; enableCommenting(enable: boolean): void; registerContinueOnCommentProvider(provider: IContinueOnCommentProvider): IDisposable; - removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined; + removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; uniqueOwner: string; isReply?: boolean }): PendingCommentThread | undefined; + resourceHasCommentingRanges(resource: URI): boolean; } const CONTINUE_ON_COMMENTS = 'comments.continueOnComments'; @@ -137,8 +140,8 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateNotebookCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateNotebookCommentThreads: Event = this._onDidUpdateNotebookCommentThreads.event; - private readonly _onDidUpdateCommentingRanges: Emitter<{ owner: string }> = this._register(new Emitter<{ owner: string }>()); - readonly onDidUpdateCommentingRanges: Event<{ owner: string }> = this._onDidUpdateCommentingRanges.event; + private readonly _onDidUpdateCommentingRanges: Emitter<{ uniqueOwner: string }> = this._register(new Emitter<{ uniqueOwner: string }>()); + readonly onDidUpdateCommentingRanges: Event<{ uniqueOwner: string }> = this._onDidUpdateCommentingRanges.event; private readonly _onDidChangeActiveEditingCommentThread = this._register(new Emitter()); readonly onDidChangeActiveEditingCommentThread = this._onDidChangeActiveEditingCommentThread.event; @@ -163,19 +166,23 @@ export class CommentService extends Disposable implements ICommentService { private _isCommentingEnabled: boolean = true; private _workspaceHasCommenting: IContextKey; - private _continueOnComments = new Map(); // owner -> PendingCommentThread[] + private _continueOnComments = new Map(); // uniqueOwner -> PendingCommentThread[] private _continueOnCommentProviders = new Set(); private readonly _commentsModel: CommentsModel = this._register(new CommentsModel()); public readonly commentsModel: ICommentsModel = this._commentsModel; + private _commentingRangeResources = new Set(); // URIs + private _commentingRangeResourceHintSchemes = new Set(); // schemes + constructor( @IInstantiationService protected readonly instantiationService: IInstantiationService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IModelService private readonly modelService: IModelService ) { super(); this._handleConfiguration(); @@ -194,15 +201,16 @@ export class CommentService extends Disposable implements ICommentService { } this.logService.debug(`Comments: URIs of continue on comments from storage ${commentsToRestore.map(thread => thread.uri.toString()).join(', ')}.`); const changedOwners = this._addContinueOnComments(commentsToRestore, this._continueOnComments); - for (const owner of changedOwners) { - const control = this._commentControls.get(owner); + for (const uniqueOwner of changedOwners) { + const control = this._commentControls.get(uniqueOwner); if (!control) { continue; } const evt: ICommentThreadChangedEvent = { - owner, + uniqueOwner: uniqueOwner, + owner: control.owner, ownerLabel: control.label, - pending: this._continueOnComments.get(owner) || [], + pending: this._continueOnComments.get(uniqueOwner) || [], added: [], removed: [], changed: [] @@ -218,6 +226,21 @@ export class CommentService extends Disposable implements ICommentService { } this._saveContinueOnComments(map); })); + + this._register(this.modelService.onModelAdded(model => { + // Allows comment providers to cause their commenting ranges to be prefetched by opening text documents in the background. + if (!this._commentingRangeResources.has(model.uri.toString())) { + this.getDocumentComments(model.uri); + } + })); + } + + private _updateResourcesWithCommentingRanges(resource: URI, commentInfos: (ICommentInfo | null)[]) { + for (const comments of commentInfos) { + if (comments && (comments.commentingRanges.ranges.length > 0 || comments.threads.length > 0)) { + this._commentingRangeResources.add(resource.toString()); + } + } } private _handleConfiguration() { @@ -273,8 +296,8 @@ export class CommentService extends Disposable implements ICommentService { } private _lastActiveCommentController: ICommentController | undefined; - async setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined) { - const commentController = this._commentControls.get(owner); + async setActiveCommentAndThread(uniqueOwner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined) { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -291,8 +314,8 @@ export class CommentService extends Disposable implements ICommentService { this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); } - private setModelThreads(ownerId: string, ownerLabel: string, commentThreads: CommentThread[]) { - this._commentsModel.setCommentThreads(ownerId, ownerLabel, commentThreads); + private setModelThreads(ownerId: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]) { + this._commentsModel.setCommentThreads(ownerId, owner, ownerLabel, commentThreads); this._onDidSetAllCommentThreads.fire({ ownerId, ownerLabel, commentThreads }); } @@ -301,45 +324,45 @@ export class CommentService extends Disposable implements ICommentService { this._onDidUpdateCommentThreads.fire(event); } - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void { + setWorkspaceComments(uniqueOwner: string, commentsByResource: CommentThread[]): void { if (commentsByResource.length) { this._workspaceHasCommenting.set(true); } - const control = this._commentControls.get(owner); + const control = this._commentControls.get(uniqueOwner); if (control) { - this.setModelThreads(owner, control.label, commentsByResource); + this.setModelThreads(uniqueOwner, control.owner, control.label, commentsByResource); } } - removeWorkspaceComments(owner: string): void { - const control = this._commentControls.get(owner); + removeWorkspaceComments(uniqueOwner: string): void { + const control = this._commentControls.get(uniqueOwner); if (control) { - this.setModelThreads(owner, control.label, []); + this.setModelThreads(uniqueOwner, control.owner, control.label, []); } } - registerCommentController(owner: string, commentControl: ICommentController): void { - this._commentControls.set(owner, commentControl); + registerCommentController(uniqueOwner: string, commentControl: ICommentController): void { + this._commentControls.set(uniqueOwner, commentControl); this._onDidSetDataProvider.fire(); } - unregisterCommentController(owner?: string): void { - if (owner) { - this._commentControls.delete(owner); + unregisterCommentController(uniqueOwner?: string): void { + if (uniqueOwner) { + this._commentControls.delete(uniqueOwner); } else { this._commentControls.clear(); } - this._commentsModel.deleteCommentsByOwner(owner); - this._onDidDeleteDataProvider.fire(owner); + this._commentsModel.deleteCommentsByOwner(uniqueOwner); + this._onDidDeleteDataProvider.fire(uniqueOwner); } - getCommentController(owner: string): ICommentController | undefined { - return this._commentControls.get(owner); + getCommentController(uniqueOwner: string): ICommentController | undefined { + return this._commentControls.get(uniqueOwner); } - async createCommentThreadTemplate(owner: string, resource: URI, range: Range | undefined): Promise { - const commentController = this._commentControls.get(owner); + async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -348,8 +371,8 @@ export class CommentService extends Disposable implements ICommentService { return commentController.createCommentThreadTemplate(resource, range); } - async updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range) { - const commentController = this._commentControls.get(owner); + async updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range) { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -358,41 +381,46 @@ export class CommentService extends Disposable implements ICommentService { await commentController.updateCommentThreadTemplate(threadHandle, range); } - disposeCommentThread(owner: string, threadId: string) { - const controller = this.getCommentController(owner); + disposeCommentThread(uniqueOwner: string, threadId: string) { + const controller = this.getCommentController(uniqueOwner); controller?.deleteCommentThreadMain(threadId); } - getCommentMenus(owner: string): CommentMenus { - if (this._commentMenus.get(owner)) { - return this._commentMenus.get(owner)!; + getCommentMenus(uniqueOwner: string): CommentMenus { + if (this._commentMenus.get(uniqueOwner)) { + return this._commentMenus.get(uniqueOwner)!; } const menu = this.instantiationService.createInstance(CommentMenus); - this._commentMenus.set(owner, menu); + this._commentMenus.set(uniqueOwner, menu); return menu; } updateComments(ownerId: string, event: CommentThreadChangedEvent): void { const control = this._commentControls.get(ownerId); if (control) { - const evt: ICommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId, ownerLabel: control.label }); + const evt: ICommentThreadChangedEvent = Object.assign({}, event, { uniqueOwner: ownerId, ownerLabel: control.label, owner: control.owner }); this.updateModelThreads(evt); } } updateNotebookComments(ownerId: string, event: CommentThreadChangedEvent): void { - const evt: INotebookCommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId }); + const evt: INotebookCommentThreadChangedEvent = Object.assign({}, event, { uniqueOwner: ownerId }); this._onDidUpdateNotebookCommentThreads.fire(evt); } - updateCommentingRanges(ownerId: string) { + updateCommentingRanges(ownerId: string, resourceHints?: CommentingRangeResourceHint) { + if (resourceHints?.schemes && resourceHints.schemes.length > 0) { + for (const scheme of resourceHints.schemes) { + this._commentingRangeResourceHintSchemes.add(scheme); + } + } this._workspaceHasCommenting.set(true); - this._onDidUpdateCommentingRanges.fire({ owner: ownerId }); + this._onDidUpdateCommentingRanges.fire({ uniqueOwner: ownerId }); } - async toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise { - const commentController = this._commentControls.get(owner); + async toggleReaction(uniqueOwner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise { + const commentController = this._commentControls.get(uniqueOwner); if (commentController) { return commentController.toggleReaction(resource, thread, comment, reaction, CancellationToken.None); @@ -401,8 +429,8 @@ export class CommentService extends Disposable implements ICommentService { } } - hasReactionHandler(owner: string): boolean { - const commentProvider = this._commentControls.get(owner); + hasReactionHandler(uniqueOwner: string): boolean { + const commentProvider = this._commentControls.get(uniqueOwner); if (commentProvider) { return !!commentProvider.features.reactionHandler; @@ -421,10 +449,10 @@ export class CommentService extends Disposable implements ICommentService { // This can happen because continue on comments are stored separately from local un-submitted comments. for (const documentCommentThread of documentComments.threads) { if (documentCommentThread.comments?.length === 0 && documentCommentThread.range) { - this.removeContinueOnComment({ range: documentCommentThread.range, uri: resource, owner: documentComments.owner }); + this.removeContinueOnComment({ range: documentCommentThread.range, uri: resource, uniqueOwner: documentComments.uniqueOwner }); } } - const pendingComments = this._continueOnComments.get(documentComments.owner); + const pendingComments = this._continueOnComments.get(documentComments.uniqueOwner); documentComments.pendingCommentThreads = pendingComments?.filter(pendingComment => pendingComment.uri.toString() === resource.toString()); return documentComments; }) @@ -433,7 +461,9 @@ export class CommentService extends Disposable implements ICommentService { })); } - return Promise.all(commentControlResult); + const commentInfos = await Promise.all(commentControlResult); + this._updateResourcesWithCommentingRanges(resource, commentInfos); + return commentInfos; } async getNotebookComments(resource: URI): Promise<(INotebookCommentInfo | null)[]> { @@ -467,8 +497,8 @@ export class CommentService extends Disposable implements ICommentService { this.storageService.store(CONTINUE_ON_COMMENTS, commentsToSave, StorageScope.WORKSPACE, StorageTarget.USER); } - removeContinueOnComment(pendingComment: { range: IRange; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined { - const pendingComments = this._continueOnComments.get(pendingComment.owner); + removeContinueOnComment(pendingComment: { range: IRange; uri: URI; uniqueOwner: string; isReply?: boolean }): PendingCommentThread | undefined { + const pendingComments = this._continueOnComments.get(pendingComment.uniqueOwner); if (pendingComments) { const commentIndex = pendingComments.findIndex(comment => comment.uri.toString() === pendingComment.uri.toString() && Range.equalsRange(comment.range, pendingComment.range) && (pendingComment.isReply === undefined || comment.isReply === pendingComment.isReply)); if (commentIndex > -1) { @@ -481,17 +511,21 @@ export class CommentService extends Disposable implements ICommentService { private _addContinueOnComments(pendingComments: PendingCommentThread[], map: Map): Set { const changedOwners = new Set(); for (const pendingComment of pendingComments) { - if (!map.has(pendingComment.owner)) { - map.set(pendingComment.owner, [pendingComment]); - changedOwners.add(pendingComment.owner); + if (!map.has(pendingComment.uniqueOwner)) { + map.set(pendingComment.uniqueOwner, [pendingComment]); + changedOwners.add(pendingComment.uniqueOwner); } else { - const commentsForOwner = map.get(pendingComment.owner)!; + const commentsForOwner = map.get(pendingComment.uniqueOwner)!; if (commentsForOwner.every(comment => (comment.uri.toString() !== pendingComment.uri.toString()) || !Range.equalsRange(comment.range, pendingComment.range))) { commentsForOwner.push(pendingComment); - changedOwners.add(pendingComment.owner); + changedOwners.add(pendingComment.uniqueOwner); } } } return changedOwners; } + + resourceHasCommentingRanges(resource: URI): boolean { + return this._commentingRangeResourceHintSchemes.has(resource.scheme) || this._commentingRangeResources.has(resource.toString()); + } } diff --git a/code/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/code/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index b206d26b011..9784625cd2f 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -22,6 +22,7 @@ import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { MarshalledCommentThread } from 'vs/workbench/common/comments'; const collapseIcon = registerIcon('review-comment-collapse', Codicon.chevronUp, nls.localize('collapseIcon', 'Icon to collapse a review comment.')); const COLLAPSE_ACTION_CLASS = 'expand-review-action ' + ThemeIcon.asClassName(collapseIcon); @@ -122,7 +123,7 @@ export class CommentThreadHeader extends Disposable { getAnchor: () => event, getActions: () => actions, actionRunner: new ActionRunner(), - getActionsContext: () => { + getActionsContext: (): MarshalledCommentThread => { return { commentControlHandle: this._commentThread.controllerHandle, commentThreadHandle: this._commentThread.commentThreadHandle, diff --git a/code/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/code/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index bcd9366e524..e09cd4f5167 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -351,7 +351,7 @@ export class CommentThreadWidget extends } focusCommentEditor() { - this._commentReply?.focusCommentEditor(); + this._commentReply?.expandReplyAreaAndFocusCommentEditor(); } focus() { diff --git a/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index e5ae9040d50..a3c6f70f09a 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -31,6 +31,12 @@ function getCommentThreadWidgetStateColor(thread: languages.CommentThreadState | return getCommentThreadStateBorderColor(thread, theme) ?? theme.getColor(peekViewBorder); } +export enum CommentWidgetFocus { + None = 0, + Widget = 1, + Editor = 2 +} + export function parseMouseDownInfoFromEvent(e: IEditorMouseEvent) { const range = e.target.range; @@ -105,8 +111,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _contextKeyService: IContextKeyService; private _scopedInstantiationService: IInstantiationService; - public get owner(): string { - return this._owner; + public get uniqueOwner(): string { + return this._uniqueOwner; } public get commentThread(): languages.CommentThread { return this._commentThread; @@ -120,7 +126,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget constructor( editor: ICodeEditor, - private _owner: string, + private _uniqueOwner: string, private _commentThread: languages.CommentThread, private _pendingComment: string | undefined, private _pendingEdits: { [key: number]: string } | undefined, @@ -137,7 +143,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget [IContextKeyService, this._contextKeyService] )); - const controller = this.commentService.getCommentController(this._owner); + const controller = this.commentService.getCommentController(this._uniqueOwner); if (controller) { this._commentOptions = controller.options; } @@ -181,7 +187,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget // we don't do anything here as we always do the reveal ourselves. } - public reveal(commentUniqueId?: number, focus: boolean = false) { + public reveal(commentUniqueId?: number, focus: CommentWidgetFocus = CommentWidgetFocus.None) { if (!this._isExpanded) { this.show(this.arrowPosition(this._commentThread.range), 2); } @@ -197,16 +203,20 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget scrollTop = this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top; } this.editor.setScrollTop(scrollTop); - if (focus) { + if (focus === CommentWidgetFocus.Widget) { this._commentThreadWidget.focus(); + } else if (focus === CommentWidgetFocus.Editor) { + this._commentThreadWidget.focusCommentEditor(); } return; } } this.editor.revealRangeInCenter(this._commentThread.range ?? new Range(1, 1, 1, 1)); - if (focus) { + if (focus === CommentWidgetFocus.Widget) { this._commentThreadWidget.focus(); + } else if (focus === CommentWidgetFocus.Editor) { + this._commentThreadWidget.focusCommentEditor(); } } @@ -229,7 +239,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget CommentThreadWidget, container, this.editor, - this._owner, + this._uniqueOwner, this.editor.getModel()!.uri, this._contextKeyService, this._scopedInstantiationService, @@ -258,7 +268,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } else { range = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.endColumn); } - await this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, range); + await this.commentService.updateCommentThreadTemplate(this.uniqueOwner, this._commentThread.commentThreadHandle, range); } } }, @@ -281,7 +291,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private deleteCommentThread(): void { this.dispose(); - this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId); + this.commentService.disposeCommentThread(this.uniqueOwner, this._commentThread.threadId); } public collapse() { diff --git a/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index b47cdb2b883..e57e7c315e2 100644 --- a/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/code/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -17,10 +17,14 @@ import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/co import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { CommentThreadState } from 'vs/editor/common/languages'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { CONTEXT_KEY_HAS_COMMENTS, CONTEXT_KEY_SOME_COMMENTS_EXPANDED, CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { Codicon } from 'vs/base/common/codicons'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; +import { MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; registerAction2(class Collapse extends ViewAction { constructor() { @@ -64,6 +68,28 @@ registerAction2(class Expand extends ViewAction { } }); +registerAction2(class Reply extends Action2 { + constructor() { + super({ + id: 'comments.reply', + title: nls.localize('reply', "Reply"), + icon: Codicon.reply, + menu: { + id: MenuId.CommentsViewThreadActions, + order: 100, + when: ContextKeyExpr.equals('canReply', true) + }, + }); + } + + override run(accessor: ServicesAccessor, marshalledCommentThread: MarshalledCommentThreadInternal): void { + const commentService = accessor.get(ICommentService); + const editorService = accessor.get(IEditorService); + const uriIdentityService = accessor.get(IUriIdentityService); + revealCommentThread(commentService, editorService, uriIdentityService, marshalledCommentThread.thread, marshalledCommentThread.thread.comments![marshalledCommentThread.thread.comments!.length - 1], true); + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'comments', order: 20, diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsController.ts b/code/src/vs/workbench/contrib/comments/browser/commentsController.ts index 4faf49116c0..7abfde48305 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -10,12 +10,12 @@ import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/com import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/review'; -import { ICodeEditor, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IEditorMouseEvent, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { EditorType, IDiffEditor, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorType, IDiffEditor, IEditor, IEditorContribution, IModelChangedEvent } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, IModelDeltaDecoration } from 'vs/editor/common/model'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import * as languages from 'vs/editor/common/languages'; import * as nls from 'vs/nls'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -23,8 +23,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { isMouseUpEventDragFromMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadZoneWidget'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { CommentWidgetFocus, isMouseUpEventDragFromMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadZoneWidget'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -45,6 +45,8 @@ import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/commo import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { URI } from 'vs/base/common/uri'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; export const ID = 'editor.contrib.review'; @@ -203,10 +205,10 @@ class CommentingRangeDecorator { intersectingEmphasisRange = new Range(intersectingSelectionRange.endLineNumber, 1, intersectingSelectionRange.endLineNumber, 1); intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber, 1, intersectingSelectionRange.endLineNumber - 1, 1); } - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); if (!this._lineHasThread(editor, intersectingEmphasisRange)) { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); } const beforeRangeEndLine = Math.min(intersectingEmphasisRange.startLineNumber, intersectingSelectionRange.startLineNumber) - 1; @@ -215,27 +217,27 @@ class CommentingRangeDecorator { const hasAfterRange = rangeObject.endLineNumber >= afterRangeStartLine; if (hasBeforeRange) { const beforeRange = new Range(range.startLineNumber, 1, beforeRangeEndLine, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); } if (hasAfterRange) { const afterRange = new Range(afterRangeStartLine, 1, range.endLineNumber, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } } else if ((rangeObject.startLineNumber <= emphasisLine) && (emphasisLine <= rangeObject.endLineNumber)) { if (rangeObject.startLineNumber < emphasisLine) { const beforeRange = new Range(range.startLineNumber, 1, emphasisLine - 1, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); } const emphasisRange = new Range(emphasisLine, 1, emphasisLine, 1); if (!this._lineHasThread(editor, emphasisRange)) { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, emphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, emphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); } if (emphasisLine < rangeObject.endLineNumber) { const afterRange = new Range(emphasisLine + 1, 1, range.endLineNumber, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } } else { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); } }); } @@ -274,7 +276,7 @@ class CommentingRangeDecorator { return foundInfos.map(foundInfo => { return { action: { - ownerId: foundInfo.owner, + ownerId: foundInfo.uniqueOwner, extensionId: foundInfo.extensionId, label: foundInfo.label, commentingRangesInfo: foundInfo.commentingRanges @@ -290,7 +292,7 @@ class CommentingRangeDecorator { for (const decoration of this.commentingRangeDecorations) { const range = decoration.getActiveRange(); if (range && this.areRangesIntersectingOrTouchingByLine(range, commentRange)) { - // We can have several commenting ranges that match from the same owner because of how + // We can have several commenting ranges that match from the same uniqueOwner because of how // the line hover and selection decoration is done. // The ranges must be merged so that we can see if the new commentRange fits within them. const action = decoration.getCommentAction(); @@ -366,6 +368,57 @@ class CommentingRangeDecorator { } } +export function revealCommentThread(commentService: ICommentService, editorService: IEditorService, uriIdentityService: IUriIdentityService, + commentThread: languages.CommentThread, comment: languages.Comment, focusReply?: boolean, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void { + if (!commentThread.resource) { + return; + } + if (!commentService.isCommentingEnabled) { + commentService.enableCommenting(true); + } + + const range = commentThread.range; + const focus = focusReply ? CommentWidgetFocus.Editor : (preserveFocus ? CommentWidgetFocus.None : CommentWidgetFocus.Widget); + + const activeEditor = editorService.activeTextEditorControl; + // If the active editor is a diff editor where one of the sides has the comment, + // then we try to reveal the comment in the diff editor. + const currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()] + : (activeEditor ? [activeEditor] : []); + const threadToReveal = commentThread.threadId; + const commentToReveal = comment.uniqueIdInThread; + const resource = URI.parse(commentThread.resource); + + for (const editor of currentActiveResources) { + const model = editor.getModel(); + if ((model instanceof TextModel) && uriIdentityService.extUri.isEqual(resource, model.uri)) { + + if (threadToReveal && isCodeEditor(editor)) { + const controller = CommentController.get(editor); + controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus); + } + return; + } + } + + editorService.openEditor({ + resource, + options: { + pinned: pinned, + preserveFocus: preserveFocus, + selection: range ?? new Range(1, 1, 1, 1) + } + } as ITextResourceEditorInput, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { + if (editor) { + const control = editor.getControl(); + if (threadToReveal && isCodeEditor(control)) { + const controller = CommentController.get(control); + controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus); + } + } + }); +} + export class CommentController implements IEditorContribution { private readonly globalToDispose = new DisposableStore(); private readonly localToDispose = new DisposableStore(); @@ -376,13 +429,14 @@ export class CommentController implements IEditorContribution { private _commentThreadRangeDecorator!: CommentThreadRangeDecorator; private mouseDownInfo: { lineNumber: number } | null = null; private _commentingRangeSpaceReserved = false; + private _commentingRangeAmountReserved = 0; private _computePromise: CancelablePromise> | null; private _addInProgress!: boolean; private _emptyThreadsToAddQueue: [Range | undefined, IEditorMouseEvent | undefined][] = []; private _computeCommentingRangePromise!: CancelablePromise | null; private _computeCommentingRangeScheduler!: Delayer> | null; private _pendingNewCommentCache: { [key: string]: { [key: string]: string } }; - private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: string } } }; // owner -> threadId -> uniqueIdInThread -> pending comment + private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: string } } }; // uniqueOwner -> threadId -> uniqueIdInThread -> pending comment private _inProcessContinueOnComments: Map = new Map(); private _editorDisposables: IDisposable[] = []; private _activeCursorHasCommentingRange: IContextKey; @@ -462,6 +516,7 @@ export class CommentController implements IEditorContribution { } })); + this.globalToDispose.add(this.editor.onWillChangeModel(e => this.onWillChangeModel(e))); this.globalToDispose.add(this.editor.onDidChangeModel(_ => this.onModelChanged())); this.globalToDispose.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('diffEditor.renderSideBySide')) { @@ -494,7 +549,7 @@ export class CommentController implements IEditorContribution { if (pendingNewComment !== lastCommentBody) { pendingComments.push({ - owner: zone.owner, + uniqueOwner: zone.uniqueOwner, uri: zone.editor.getModel()!.uri, range: zone.commentThread.range, body: pendingNewComment, @@ -628,7 +683,7 @@ export class CommentController implements IEditorContribution { return editor.getContribution(ID); } - public revealCommentThread(threadId: string, commentUniqueId: number, fetchOnceIfNotExist: boolean, focus: boolean): void { + public revealCommentThread(threadId: string, commentUniqueId: number, fetchOnceIfNotExist: boolean, focus: CommentWidgetFocus): void { const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId); if (commentThreadWidget.length === 1) { commentThreadWidget[0].reveal(commentUniqueId, focus); @@ -732,7 +787,7 @@ export class CommentController implements IEditorContribution { nextWidget = sortedWidgets[idx]; } this.editor.setSelection(nextWidget.commentThread.range ?? new Range(1, 1, 1, 1)); - nextWidget.reveal(undefined, true); + nextWidget.reveal(undefined, CommentWidgetFocus.Widget); } public previousCommentThread(): void { @@ -778,8 +833,15 @@ export class CommentController implements IEditorContribution { this.editor = null!; // Strict null override - nulling out in dispose } + private onWillChangeModel(e: IModelChangedEvent): void { + if (e.newModelUrl) { + this.tryUpdateReservedSpace(e.newModelUrl); + } + } + public onModelChanged(): void { this.localToDispose.clear(); + this.tryUpdateReservedSpace(); this.removeCommentWidgetsAndStoreCache(); if (!this.editor) { @@ -815,7 +877,7 @@ export class CommentController implements IEditorContribution { await this._computePromise; } - const commentInfo = this._commentInfos.filter(info => info.owner === e.owner); + const commentInfo = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner); if (!commentInfo || !commentInfo.length) { return; } @@ -826,14 +888,14 @@ export class CommentController implements IEditorContribution { const pending = e.pending.filter(pending => pending.uri.toString() === editorURI.toString()); removed.forEach(thread => { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); if (matchedZones.length) { const matchedZone = matchedZones[0]; const index = this._commentWidgets.indexOf(matchedZone); this._commentWidgets.splice(index, 1); matchedZone.dispose(); } - const infosThreads = this._commentInfos.filter(info => info.owner === e.owner)[0].threads; + const infosThreads = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads; for (let i = 0; i < infosThreads.length; i++) { if (infosThreads[i] === thread) { infosThreads.splice(i, 1); @@ -843,7 +905,7 @@ export class CommentController implements IEditorContribution { }); changed.forEach(thread => { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { const matchedZone = matchedZones[0]; matchedZone.update(thread); @@ -851,19 +913,19 @@ export class CommentController implements IEditorContribution { } }); for (const thread of added) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { return; } - const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); if (matchedNewCommentThreadZones.length) { matchedNewCommentThreadZones[0].update(thread); return; } - const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.owner)?.findIndex(pending => { + const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.uniqueOwner)?.findIndex(pending => { if (pending.range === undefined) { return thread.range === undefined; } else { @@ -872,14 +934,14 @@ export class CommentController implements IEditorContribution { }); let continueOnCommentText: string | undefined; if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { - continueOnCommentText = this._inProcessContinueOnComments.get(e.owner)?.splice(continueOnCommentIndex, 1)[0].body; + continueOnCommentText = this._inProcessContinueOnComments.get(e.uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; } - const pendingCommentText = (this._pendingNewCommentCache[e.owner] && this._pendingNewCommentCache[e.owner][thread.threadId]) + const pendingCommentText = (this._pendingNewCommentCache[e.uniqueOwner] && this._pendingNewCommentCache[e.uniqueOwner][thread.threadId]) ?? continueOnCommentText; - const pendingEdits = this._pendingEditsCache[e.owner] && this._pendingEditsCache[e.owner][thread.threadId]; - this.displayCommentThread(e.owner, thread, pendingCommentText, pendingEdits); - this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); + const pendingEdits = this._pendingEditsCache[e.uniqueOwner] && this._pendingEditsCache[e.uniqueOwner][thread.threadId]; + this.displayCommentThread(e.uniqueOwner, thread, pendingCommentText, pendingEdits); + this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads.push(thread); this.tryUpdateReservedSpace(); } @@ -893,12 +955,12 @@ export class CommentController implements IEditorContribution { } private async resumePendingComment(editorURI: URI, thread: languages.PendingCommentThread) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === thread.owner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range)); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === thread.uniqueOwner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range)); if (thread.isReply && matchedZones.length) { - this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: true }); + this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: true }); matchedZones[0].setPendingComment(thread.body); } else if (matchedZones.length) { - this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: false }); + this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false }); const existingPendingComment = matchedZones[0].getPendingComments().newComment; // We need to try to reconcile the existing pending comment with the incoming pending comment let pendingComment: string; @@ -911,15 +973,15 @@ export class CommentController implements IEditorContribution { } matchedZones[0].setPendingComment(pendingComment); } else if (!thread.isReply) { - const threadStillAvailable = this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: false }); + const threadStillAvailable = this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false }); if (!threadStillAvailable) { return; } - if (!this._inProcessContinueOnComments.has(thread.owner)) { - this._inProcessContinueOnComments.set(thread.owner, []); + if (!this._inProcessContinueOnComments.has(thread.uniqueOwner)) { + this._inProcessContinueOnComments.set(thread.uniqueOwner, []); } - this._inProcessContinueOnComments.get(thread.owner)?.push(thread); - await this.commentService.createCommentThreadTemplate(thread.owner, thread.uri, thread.range ? Range.lift(thread.range) : undefined); + this._inProcessContinueOnComments.get(thread.uniqueOwner)?.push(thread); + await this.commentService.createCommentThreadTemplate(thread.uniqueOwner, thread.uri, thread.range ? Range.lift(thread.range) : undefined); } } @@ -959,7 +1021,7 @@ export class CommentController implements IEditorContribution { return undefined; } - private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { + private displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { const editor = this.editor?.getModel(); if (!editor) { return; @@ -970,9 +1032,9 @@ export class CommentController implements IEditorContribution { let continueOnCommentReply: languages.PendingCommentThread | undefined; if (thread.range && !pendingComment) { - continueOnCommentReply = this.commentService.removeContinueOnComment({ owner, uri: editor.uri, range: thread.range, isReply: true }); + continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true }); } - const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); + const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); zoneWidget.display(thread.range); this._commentWidgets.push(zoneWidget); this.openCommentsView(thread); @@ -1171,15 +1233,20 @@ export class CommentController implements IEditorContribution { return { extraEditorClassName, lineDecorationsWidth }; } - private getWithCommentsEditorOptions(editor: ICodeEditor, extraEditorClassName: string[], startingLineDecorationsWidth: number) { + private getWithCommentsLineDecorationWidth(editor: ICodeEditor, startingLineDecorationsWidth: number) { let lineDecorationsWidth = startingLineDecorationsWidth; const options = editor.getOptions(); if (options.get(EditorOption.folding) && options.get(EditorOption.showFoldingControls) !== 'never') { lineDecorationsWidth -= 11; } lineDecorationsWidth += 24; + this._commentingRangeAmountReserved = lineDecorationsWidth; + return this._commentingRangeAmountReserved; + } + + private getWithCommentsEditorOptions(editor: ICodeEditor, extraEditorClassName: string[], startingLineDecorationsWidth: number) { extraEditorClassName.push('inline-comment'); - return { lineDecorationsWidth, extraEditorClassName }; + return { lineDecorationsWidth: this.getWithCommentsLineDecorationWidth(editor, startingLineDecorationsWidth), extraEditorClassName }; } private updateEditorLayoutOptions(editor: ICodeEditor, extraEditorClassName: string[], lineDecorationsWidth: number) { @@ -1189,21 +1256,38 @@ export class CommentController implements IEditorContribution { }); } - private tryUpdateReservedSpace() { + private ensureCommentingRangeReservedAmount(editor: ICodeEditor) { + const existing = this.getExistingCommentEditorOptions(editor); + if (existing.lineDecorationsWidth !== this._commentingRangeAmountReserved) { + editor.updateOptions({ + lineDecorationsWidth: this.getWithCommentsLineDecorationWidth(editor, existing.lineDecorationsWidth) + }); + } + } + + private tryUpdateReservedSpace(uri?: URI) { if (!this.editor) { return; } - const hasCommentsOrRanges = this._commentInfos.some(info => { + const hasCommentsOrRangesInInfo = this._commentInfos.some(info => { const hasRanges = Boolean(info.commentingRanges && (Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges.ranges).length); return hasRanges || (info.threads.length > 0); }); + uri = uri ?? this.editor.getModel()?.uri; + const resourceHasCommentingRanges = uri ? this.commentService.resourceHasCommentingRanges(uri) : false; - if (hasCommentsOrRanges && !this._commentingRangeSpaceReserved && this.commentService.isCommentingEnabled) { - this._commentingRangeSpaceReserved = true; - const { lineDecorationsWidth, extraEditorClassName } = this.getExistingCommentEditorOptions(this.editor); - const newOptions = this.getWithCommentsEditorOptions(this.editor, extraEditorClassName, lineDecorationsWidth); - this.updateEditorLayoutOptions(this.editor, newOptions.extraEditorClassName, newOptions.lineDecorationsWidth); + const hasCommentsOrRanges = hasCommentsOrRangesInInfo || resourceHasCommentingRanges; + + if (hasCommentsOrRanges && this.commentService.isCommentingEnabled) { + if (!this._commentingRangeSpaceReserved) { + this._commentingRangeSpaceReserved = true; + const { lineDecorationsWidth, extraEditorClassName } = this.getExistingCommentEditorOptions(this.editor); + const newOptions = this.getWithCommentsEditorOptions(this.editor, extraEditorClassName, lineDecorationsWidth); + this.updateEditorLayoutOptions(this.editor, newOptions.extraEditorClassName, newOptions.lineDecorationsWidth); + } else { + this.ensureCommentingRangeReservedAmount(this.editor); + } } else if ((!hasCommentsOrRanges || !this.commentService.isCommentingEnabled) && this._commentingRangeSpaceReserved) { this._commentingRangeSpaceReserved = false; const { lineDecorationsWidth, extraEditorClassName } = this.getExistingCommentEditorOptions(this.editor); @@ -1228,8 +1312,8 @@ export class CommentController implements IEditorContribution { hasCommentingRanges = true; } - const providerCacheStore = this._pendingNewCommentCache[info.owner]; - const providerEditsCacheStore = this._pendingEditsCache[info.owner]; + const providerCacheStore = this._pendingNewCommentCache[info.uniqueOwner]; + const providerEditsCacheStore = this._pendingEditsCache[info.uniqueOwner]; info.threads = info.threads.filter(thread => !thread.isDisposed); info.threads.forEach(thread => { let pendingComment: string | undefined = undefined; @@ -1242,7 +1326,7 @@ export class CommentController implements IEditorContribution { pendingEdits = providerEditsCacheStore[thread.threadId]; } - this.displayCommentThread(info.owner, thread, pendingComment, pendingEdits); + this.displayCommentThread(info.uniqueOwner, thread, pendingComment, pendingEdits); }); for (const thread of info.pendingCommentThreads ?? []) { this.resumePendingComment(this.editor!.getModel()!.uri, thread); @@ -1272,7 +1356,7 @@ export class CommentController implements IEditorContribution { this._commentWidgets.forEach(zone => { const pendingComments = zone.getPendingComments(); const pendingNewComment = pendingComments.newComment; - const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.owner]; + const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.uniqueOwner]; let lastCommentBody; if (zone.commentThread.comments && zone.commentThread.comments.length) { @@ -1285,10 +1369,10 @@ export class CommentController implements IEditorContribution { } if (pendingNewComment && (pendingNewComment !== lastCommentBody)) { if (!providerNewCommentCacheStore) { - this._pendingNewCommentCache[zone.owner] = {}; + this._pendingNewCommentCache[zone.uniqueOwner] = {}; } - this._pendingNewCommentCache[zone.owner][zone.commentThread.threadId] = pendingNewComment; + this._pendingNewCommentCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingNewComment; } else { if (providerNewCommentCacheStore) { delete providerNewCommentCacheStore[zone.commentThread.threadId]; @@ -1296,12 +1380,12 @@ export class CommentController implements IEditorContribution { } const pendingEdits = pendingComments.edits; - const providerEditsCacheStore = this._pendingEditsCache[zone.owner]; + const providerEditsCacheStore = this._pendingEditsCache[zone.uniqueOwner]; if (Object.keys(pendingEdits).length > 0) { if (!providerEditsCacheStore) { - this._pendingEditsCache[zone.owner] = {}; + this._pendingEditsCache[zone.uniqueOwner] = {}; } - this._pendingEditsCache[zone.owner][zone.commentThread.threadId] = pendingEdits; + this._pendingEditsCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingEdits; } else if (providerEditsCacheStore) { delete providerEditsCacheStore[zone.commentThread.threadId]; } diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsModel.ts b/code/src/vs/workbench/contrib/comments/browser/commentsModel.ts index 6d345350e83..d0701d5f344 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsModel.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsModel.ts @@ -43,15 +43,15 @@ export class CommentsModel extends Disposable implements ICommentsModel { }); } - public setCommentThreads(owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { - this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(owner, commentThreads) }); + public setCommentThreads(uniqueOwner: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { + this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(uniqueOwner, owner, commentThreads) }); this.updateResourceCommentThreads(); } - public deleteCommentsByOwner(owner?: string): void { - if (owner) { - const existingOwner = this.commentThreadsMap.get(owner); - this.commentThreadsMap.set(owner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] }); + public deleteCommentsByOwner(uniqueOwner?: string): void { + if (uniqueOwner) { + const existingOwner = this.commentThreadsMap.get(uniqueOwner); + this.commentThreadsMap.set(uniqueOwner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] }); } else { this.commentThreadsMap.clear(); } @@ -59,9 +59,9 @@ export class CommentsModel extends Disposable implements ICommentsModel { } public updateCommentThreads(event: ICommentThreadChangedEvent): boolean { - const { owner, ownerLabel, removed, changed, added } = event; + const { uniqueOwner, owner, ownerLabel, removed, changed, added } = event; - const threadsForOwner = this.commentThreadsMap.get(owner)?.resourceWithCommentThreads || []; + const threadsForOwner = this.commentThreadsMap.get(uniqueOwner)?.resourceWithCommentThreads || []; removed.forEach(thread => { // Find resource that has the comment thread @@ -91,9 +91,9 @@ export class CommentsModel extends Disposable implements ICommentsModel { // Find comment node on resource that is that thread and replace it const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId); if (index >= 0) { - matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread); + matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread); } else if (thread.comments && thread.comments.length) { - matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread)); + matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread)); } }); @@ -102,14 +102,14 @@ export class CommentsModel extends Disposable implements ICommentsModel { if (existingResource.length) { const resource = existingResource[0]; if (thread.comments && thread.comments.length) { - resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, resource.resource, thread)); + resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource.resource, thread)); } } else { - threadsForOwner.push(new ResourceWithCommentThreads(owner, URI.parse(thread.resource!), [thread])); + threadsForOwner.push(new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(thread.resource!), [thread])); } }); - this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: threadsForOwner }); + this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: threadsForOwner }); this.updateResourceCommentThreads(); return removed.length > 0 || changed.length > 0 || added.length > 0; @@ -127,11 +127,11 @@ export class CommentsModel extends Disposable implements ICommentsModel { } } - private groupByResource(owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] { + private groupByResource(uniqueOwner: string, owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] { const resourceCommentThreads: ResourceWithCommentThreads[] = []; const commentThreadsByResource = new Map(); for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) { - commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(owner, URI.parse(group[0].resource!), group)); + commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(group[0].resource!), group)); } commentThreadsByResource.forEach((v, i, m) => { diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/code/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 585d253442c..1c0c588d215 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -10,7 +10,7 @@ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentNode, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel'; -import { ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { ITreeContextMenuEvent, ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -22,7 +22,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { commentViewThreadStateColorVar, getCommentThreadStateIconColor } from 'vs/workbench/contrib/comments/browser/commentColors'; -import { CommentThreadState } from 'vs/editor/common/languages'; +import { CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages'; import { Color } from 'vs/base/common/color'; import { IMatch } from 'vs/base/common/filters'; import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions'; @@ -32,8 +32,17 @@ import { IStyleOverride } from 'vs/platform/theme/browser/defaultStyles'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { CommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; +import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MarshalledCommentThread, MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_STORAGE_ID = 'Comments'; @@ -47,6 +56,7 @@ interface IResourceTemplateData { interface ICommentThreadTemplateData { threadMetadata: { + relevance: HTMLElement; icon: HTMLElement; userNames: HTMLSpanElement; timestamp: TimestampWidget; @@ -62,6 +72,7 @@ interface ICommentThreadTemplateData { separator: HTMLElement; timestamp: TimestampWidget; }; + actionBar: ActionBar; disposables: IDisposable[]; } @@ -124,29 +135,85 @@ export class ResourceWithCommentsRenderer implements IListRenderer, ICommentThreadTemplateData> { templateId: string = 'comment-node'; constructor( + private actionViewItemProvider: IActionViewItemProvider, + private menus: CommentsMenus, @IOpenerService private readonly openerService: IOpenerService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeService private themeService: IThemeService ) { } renderTemplate(container: HTMLElement) { - const threadContainer = dom.append(container, dom.$('.comment-thread-container')); const metadataContainer = dom.append(threadContainer, dom.$('.comment-metadata-container')); + const metadata = dom.append(metadataContainer, dom.$('.comment-metadata')); const threadMetadata = { - icon: dom.append(metadataContainer, dom.$('.icon')), - userNames: dom.append(metadataContainer, dom.$('.user')), - timestamp: new TimestampWidget(this.configurationService, dom.append(metadataContainer, dom.$('.timestamp-container'))), - separator: dom.append(metadataContainer, dom.$('.separator')), - commentPreview: dom.append(metadataContainer, dom.$('.text')), - range: dom.append(metadataContainer, dom.$('.range')) + icon: dom.append(metadata, dom.$('.icon')), + userNames: dom.append(metadata, dom.$('.user')), + timestamp: new TimestampWidget(this.configurationService, dom.append(metadata, dom.$('.timestamp-container'))), + relevance: dom.append(metadata, dom.$('.relevance')), + separator: dom.append(metadata, dom.$('.separator')), + commentPreview: dom.append(metadata, dom.$('.text')), + range: dom.append(metadata, dom.$('.range')) }; threadMetadata.separator.innerText = '\u00b7'; + const actionsContainer = dom.append(metadataContainer, dom.$('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionViewItemProvider: this.actionViewItemProvider + }); + const snippetContainer = dom.append(threadContainer, dom.$('.comment-snippet-container')); const repliesMetadata = { container: snippetContainer, @@ -158,9 +225,9 @@ export class CommentNodeRenderer implements IListRenderer }; repliesMetadata.separator.innerText = '\u00b7'; repliesMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.indent)); - const disposables = [threadMetadata.timestamp, repliesMetadata.timestamp]; - return { threadMetadata, repliesMetadata, disposables }; + const disposables = [threadMetadata.timestamp, repliesMetadata.timestamp]; + return { threadMetadata, repliesMetadata, actionBar, disposables }; } private getCountString(commentCount: number): string { @@ -198,7 +265,19 @@ export class CommentNodeRenderer implements IListRenderer } renderElement(node: ITreeNode, index: number, templateData: ICommentThreadTemplateData, height: number | undefined): void { + templateData.actionBar.clear(); + const commentCount = node.element.replies.length + 1; + if (node.element.threadRelevance === CommentThreadApplicability.Outdated) { + templateData.threadMetadata.relevance.style.display = ''; + templateData.threadMetadata.relevance.innerText = nls.localize('outdated', "Outdated"); + templateData.threadMetadata.separator.style.display = 'none'; + } else { + templateData.threadMetadata.relevance.innerText = ''; + templateData.threadMetadata.relevance.style.display = 'none'; + templateData.threadMetadata.separator.style.display = ''; + } + templateData.threadMetadata.icon.classList.remove(...Array.from(templateData.threadMetadata.icon.classList.values()) .filter(value => value.startsWith('codicon'))); templateData.threadMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(this.getIcon(node.element.threadState))); @@ -232,6 +311,14 @@ export class CommentNodeRenderer implements IListRenderer } } + const menuActions = this.menus.getResourceActions(node.element); + templateData.actionBar.push(menuActions.actions, { icon: true, label: false }); + templateData.actionBar.context = { + commentControlHandle: node.element.controllerHandle, + commentThreadHandle: node.element.threadHandle, + $mid: MarshalledId.CommentThread + } as MarshalledCommentThread; + if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; return; @@ -250,6 +337,7 @@ export class CommentNodeRenderer implements IListRenderer disposeTemplate(templateData: ICommentThreadTemplateData): void { templateData.disposables.forEach(disposeable => disposeable.dispose()); + templateData.actionBar.dispose(); } } @@ -347,6 +435,8 @@ export class Filter implements ITreeFilter { + private readonly menus: CommentsMenus; + constructor( labels: ResourceLabels, container: HTMLElement, @@ -355,12 +445,16 @@ export class CommentsList extends WorkbenchObjectTree this.commentsOnContextMenu(e))); + } + + private commentsOnContextMenu(treeEvent: ITreeContextMenuEvent): void { + const node: CommentsModel | ResourceWithCommentThreads | CommentNode | null = treeEvent.element; + if (!(node instanceof CommentNode)) { + return; + } + const event: UIEvent = treeEvent.browserEvent; + + event.preventDefault(); + event.stopPropagation(); + + this.setFocus([node]); + const actions = this.menus.getResourceContextActions(node); + if (!actions.length) { + return; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + getActions: () => actions, + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.domFocus(); + } + }, + getActionsContext: (): MarshalledCommentThreadInternal => ({ + commentControlHandle: node.controllerHandle, + commentThreadHandle: node.threadHandle, + $mid: MarshalledId.CommentThread, + thread: node.thread + }) + }); } filterComments(): void { diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsView.ts b/code/src/vs/workbench/contrib/comments/browser/commentsView.ts index 385ac16e1dc..4a49f52f5b8 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -7,13 +7,11 @@ import 'vs/css!./media/panel'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { basename } from 'vs/base/common/resources'; -import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentsList, COMMENTS_VIEW_TITLE, Filter } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { IViewPaneOptions, FilterViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -25,18 +23,15 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IEditor } from 'vs/editor/common/editorCommon'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments'; import { CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contrib/comments/browser/commentsViewActions'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions'; -import { CommentThreadState } from 'vs/editor/common/languages'; +import { CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages'; import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { Iterable } from 'vs/base/common/iterator'; -import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsController'; -import { Range } from 'vs/editor/common/core/range'; +import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel'; @@ -192,10 +187,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this)); this._register(this.commentService.onDidDeleteDataProvider(this.onDataProviderDeleted, this)); - const styleElement = dom.createStyleSheet(container); - this.applyStyles(styleElement); - this._register(this.themeService.onDidColorThemeChange(_ => this.applyStyles(styleElement))); - this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { this.refresh(); @@ -220,33 +211,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } } - private applyStyles(styleElement: HTMLStyleElement) { - const content: string[] = []; - - const theme = this.themeService.getColorTheme(); - const linkColor = theme.getColor(textLinkForeground); - if (linkColor) { - content.push(`.comments-panel .comments-panel-container a { color: ${linkColor}; }`); - } - - const linkActiveColor = theme.getColor(textLinkActiveForeground); - if (linkActiveColor) { - content.push(`.comments-panel .comments-panel-container a:hover, a:active { color: ${linkActiveColor}; }`); - } - - const focusColor = theme.getColor(focusBorder); - if (focusColor) { - content.push(`.comments-panel .comments-panel-container a:focus { outline-color: ${focusColor}; }`); - } - - const codeTextForegroundColor = theme.getColor(textPreformatForeground); - if (codeTextForegroundColor) { - content.push(`.comments-panel .comments-panel-container .text code { color: ${codeTextForegroundColor}; }`); - } - - styleElement.textContent = content.join('\n'); - } - private async renderComments(): Promise { this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.renderMessage(); @@ -296,6 +260,46 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads()); } + private getAriaForNode(element: CommentNode) { + if (element.range) { + if (element.threadRelevance === CommentThreadApplicability.Outdated) { + return nls.localize('resourceWithCommentLabelOutdated', + "Outdated from ${0} at line {1} column {2} in {3}, source: {4}", + element.comment.userName, + element.range.startLineNumber, + element.range.startColumn, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } else { + return nls.localize('resourceWithCommentLabel', + "${0} at line {1} column {2} in {3}, source: {4}", + element.comment.userName, + element.range.startLineNumber, + element.range.startColumn, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } + } else { + if (element.threadRelevance === CommentThreadApplicability.Outdated) { + return nls.localize('resourceWithCommentLabelFileOutdated', + "Outdated from {0} in {1}, source: {2}", + element.comment.userName, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } else { + return nls.localize('resourceWithCommentLabelFile', + "{0} in {1}, source: {2}", + element.comment.userName, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } + } + } + private createTree(): void { this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, { @@ -308,7 +312,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } }, accessibilityProvider: { - getAriaLabel(element: any): string { + getAriaLabel: (element: any): string => { if (element instanceof CommentsModel) { return nls.localize('rootCommentsLabel', "Comments for current workspace"); } @@ -316,23 +320,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { return nls.localize('resourceWithCommentThreadsLabel', "Comments in {0}, full path {1}", basename(element.resource), element.resource.fsPath); } if (element instanceof CommentNode) { - if (element.range) { - return nls.localize('resourceWithCommentLabel', - "${0} at line {1} column {2} in {3}, source: {4}", - element.comment.userName, - element.range.startLineNumber, - element.range.startColumn, - basename(element.resource), - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); - } else { - return nls.localize('resourceWithCommentLabelFile', - "${0} in {1}, source: {2}", - element.comment.userName, - basename(element.resource), - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); - } + return this.getAriaForNode(element); } return ''; }, @@ -355,62 +343,17 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { })); } - private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): boolean { + private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void { if (!element) { - return false; + return; } if (!(element instanceof ResourceWithCommentThreads || element instanceof CommentNode)) { - return false; - } - - if (!this.commentService.isCommentingEnabled) { - this.commentService.enableCommenting(true); - } - - const range = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].range : element.range; - - const activeEditor = this.editorService.activeTextEditorControl; - // If the active editor is a diff editor where one of the sides has the comment, - // then we try to reveal the comment in the diff editor. - const currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()] - : (activeEditor ? [activeEditor] : []); - - for (const editor of currentActiveResources) { - const model = editor.getModel(); - if ((model instanceof TextModel) && this.uriIdentityService.extUri.isEqual(element.resource, model.uri)) { - const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; - const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; - if (threadToReveal && isCodeEditor(editor)) { - const controller = CommentController.get(editor); - controller?.revealCommentThread(threadToReveal, commentToReveal, true, !preserveFocus); - } - - return true; - } + return; } - - const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; + const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].thread : element.thread; const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment : element.comment; - - this.editorService.openEditor({ - resource: element.resource, - options: { - pinned: pinned, - preserveFocus: preserveFocus, - selection: range ?? new Range(1, 1, 1, 1) - } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { - if (editor) { - const control = editor.getControl(); - if (threadToReveal && isCodeEditor(control)) { - const controller = CommentController.get(control); - controller?.revealCommentThread(threadToReveal, commentToReveal.uniqueIdInThread, true, !preserveFocus); - } - } - }); - - return true; + return revealCommentThread(this.commentService, this.editorService, this.uriIdentityService, threadToReveal, commentToReveal, false, pinned, preserveFocus, sideBySide); } private async refresh(): Promise { diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index e6fd43f4b91..7a0f4d21531 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -123,7 +123,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleUnResolvedComments`, - title: localize('toggle unresolved', "Toggle Unresolved Comments"), + title: localize('toggle unresolved', "Show Unresolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_UNRESOLVED, @@ -148,7 +148,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleResolvedComments`, - title: localize('toggle resolved', "Toggle Resolved Comments"), + title: localize('toggle resolved', "Show Resolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_RESOLVED, diff --git a/code/src/vs/workbench/contrib/comments/browser/media/panel.css b/code/src/vs/workbench/contrib/comments/browser/media/panel.css index a349ec52490..938c658fd2d 100644 --- a/code/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/code/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -36,6 +36,11 @@ overflow: hidden; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata { + flex: 1; + display: flex; +} + .comments-panel .count, .comments-panel .user { padding-right: 5px; @@ -48,10 +53,23 @@ } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .count, +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .relevance, .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .user { min-width: fit-content; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .relevance { + border-radius: 2px; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 0px 4px 1px 4px; + font-size: 0.9em; + margin-right: 4px; + margin-top: 4px; + margin-bottom: 3px; + line-height: 14px; +} + .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text { display: flex; flex: 1; @@ -117,3 +135,34 @@ .comments-panel .hide { display: none; } + +.comments-panel .comments-panel-container .text a { + color: var(--vscode-textLink-foreground); +} + +.comments-panel .comments-panel-container .text a:hover, +.comments-panel .comments-panel-container a:active { + color: var(--vscode-textLink-activeForeground); +} + +.comments-panel .comments-panel-container .text a:focus { + outline-color: var(--vscode-focusBorder); +} + +.comments-panel .comments-panel-container .text code { + color: var(--vscode-textPreformat-foreground); +} + +.comments-panel .comments-panel-container .actions { + display: none; +} + +.comments-panel .comments-panel-container .actions .action-label { + padding: 2px; +} + +.comments-panel .monaco-list .monaco-list-row:hover .comment-metadata-container .actions, +.comments-panel .monaco-list .monaco-list-row.selected .comment-metadata-container .actions, +.comments-panel .monaco-list .monaco-list-row.focused .comment-metadata-container .actions { + display: block; +} diff --git a/code/src/vs/workbench/contrib/comments/browser/timestamp.ts b/code/src/vs/workbench/contrib/comments/browser/timestamp.ts index 2d1fcf15b48..dbfad43dfd0 100644 --- a/code/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/code/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { fromNow } from 'vs/base/common/date'; import { Disposable } from 'vs/base/common/lifecycle'; import { language } from 'vs/base/common/platform'; @@ -24,8 +24,8 @@ export class TimestampWidget extends Disposable { this._date = dom.append(container, dom.$('span.timestamp')); this._date.style.display = 'none'; this._useRelativeTime = this.useRelativeTimeSetting; - this.setTimestamp(timeStamp); this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._date, '')); + this.setTimestamp(timeStamp); } private get useRelativeTimeSetting(): boolean { diff --git a/code/src/vs/workbench/contrib/comments/common/commentModel.ts b/code/src/vs/workbench/contrib/comments/common/commentModel.ts index 9a6d8786372..fbf25f6c06b 100644 --- a/code/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/code/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -5,31 +5,38 @@ import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; -import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages'; +import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages'; export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent { + uniqueOwner: string; owner: string; ownerLabel: string; } export class CommentNode { - owner: string; - threadId: string; - range: IRange | undefined; - comment: Comment; + isRoot: boolean = false; replies: CommentNode[] = []; - resource: URI; - isRoot: boolean; - threadState?: CommentThreadState; + public readonly threadId: string; + public readonly range: IRange | undefined; + public readonly threadState: CommentThreadState | undefined; + public readonly threadRelevance: CommentThreadApplicability | undefined; + public readonly contextValue: string | undefined; + public readonly controllerHandle: number; + public readonly threadHandle: number; - constructor(owner: string, threadId: string, resource: URI, comment: Comment, range: IRange | undefined, threadState: CommentThreadState | undefined) { - this.owner = owner; - this.threadId = threadId; - this.comment = comment; - this.resource = resource; - this.range = range; - this.isRoot = false; - this.threadState = threadState; + constructor( + public readonly uniqueOwner: string, + public readonly owner: string, + public readonly resource: URI, + public readonly comment: Comment, + public readonly thread: CommentThread) { + this.threadId = thread.threadId; + this.range = thread.range; + this.threadState = thread.state; + this.threadRelevance = thread.applicability; + this.contextValue = thread.contextValue; + this.controllerHandle = thread.controllerHandle; + this.threadHandle = thread.commentThreadHandle; } hasReply(): boolean { @@ -39,21 +46,23 @@ export class CommentNode { export class ResourceWithCommentThreads { id: string; + uniqueOwner: string; owner: string; ownerLabel: string | undefined; commentThreads: CommentNode[]; // The top level comments on the file. Replys are nested under each node. resource: URI; - constructor(owner: string, resource: URI, commentThreads: CommentThread[]) { + constructor(uniqueOwner: string, owner: string, resource: URI, commentThreads: CommentThread[]) { + this.uniqueOwner = uniqueOwner; this.owner = owner; this.id = resource.toString(); this.resource = resource; - this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(owner, resource, thread)); + this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource, thread)); } - public static createCommentNode(owner: string, resource: URI, commentThread: CommentThread): CommentNode { - const { threadId, comments, range } = commentThread; - const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(owner, threadId, resource, comment, range, commentThread.state)); + public static createCommentNode(uniqueOwner: string, owner: string, resource: URI, commentThread: CommentThread): CommentNode { + const { comments } = commentThread; + const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(uniqueOwner, owner, resource, comment, commentThread)); if (commentNodes.length > 1) { commentNodes[0].replies = commentNodes.slice(1, commentNodes.length); } diff --git a/code/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/code/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index a3e171b9d40..cd5f0ddf60c 100644 --- a/code/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/code/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -49,6 +49,7 @@ class TestCommentThread implements CommentThread { class TestCommentController implements ICommentController { id: string = 'test'; label: string = 'Test Comments'; + owner: string = 'test'; features = {}; createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise { throw new Error('Method not implemented.'); diff --git a/code/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/code/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 4e677142d69..4393b22ebb0 100644 --- a/code/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/code/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getWindow } from 'vs/base/browser/dom'; +import { CodeWindow } from 'vs/base/browser/window'; +import { toAction } from 'vs/base/common/actions'; import { VSBuffer } from 'vs/base/common/buffer'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IReference } from 'vs/base/common/lifecycle'; @@ -11,18 +14,22 @@ import { basename } from 'vs/base/common/path'; import { dirname, isEqual } from 'vs/base/common/resources'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { EditorInputCapabilities, GroupIdentifier, IMoveResult, IRevertOptions, ISaveOptions, IUntypedEditorInput, Verbosity } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, IMoveResult, IRevertOptions, ISaveOptions, IUntypedEditorInput, Verbosity, createEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { IOverlayWebview, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; interface CustomEditorInputInitInfo { @@ -83,7 +90,9 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IUndoRedoService private readonly undoRedoService: IUndoRedoService, @IFileService private readonly fileService: IFileService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, ) { super({ providedId: init.viewType, viewType: init.viewType, name: '' }, webview, webviewWorkbenchService); this._editorResource = init.resource; @@ -135,7 +144,6 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { let capabilities = EditorInputCapabilities.None; capabilities |= EditorInputCapabilities.CanDropIntoEditor; - capabilities |= EditorInputCapabilities.AuxWindowUnsupported; if (!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument) { capabilities |= EditorInputCapabilities.Singleton; @@ -389,4 +397,39 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } }; } + + public override claim(claimant: unknown, targetWindow: CodeWindow, scopedContextKeyService: IContextKeyService | undefined): void { + if (typeof this.canMove(targetWindow.vscodeWindowId) === 'string') { + throw createEditorOpenError(localize('editorUnsupportedInWindow', "Unable to open the editor in this window, it contains modifications that can only be saved in the original window."), [ + toAction({ + id: 'openInOriginalWindow', + label: localize('reopenInOriginalWindow', "Open in Original Window"), + run: async () => { + const originalPart = this.editorGroupsService.getPart(this.layoutService.getContainer(getWindow(this.webview.container).window)); + const currentPart = this.editorGroupsService.getPart(this.layoutService.getContainer(targetWindow.window)); + currentPart.activeGroup.moveEditor(this, originalPart.activeGroup); + } + }) + ], { forceMessage: true }); + } + return super.claim(claimant, targetWindow, scopedContextKeyService); + } + + public override canMove(targetWindowId: number): true | string { + if (this.isModified() && this._modelRef?.object.canHotExit === false) { + const sourceWindowId = getWindow(this.webview.container).vscodeWindowId; + if (sourceWindowId !== targetWindowId) { + + // The custom editor is modified, not backed by a file and without a backup. + // We have to assume that the modified state is enclosed into the webview + // managed by an extension. As such, we cannot just move the webview + // into another window because that means, we potentally loose the modified + // state and thus trigger data loss. + + return localize('editorCannotMove', "Unable to move the editor from this window, it contains modifications that can only be saved in the this window."); + } + } + + return true; + } } diff --git a/code/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/code/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 40c47ab8a80..28efa3bf905 100644 --- a/code/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/code/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -57,6 +57,7 @@ export interface ICustomEditorModel extends IDisposable { readonly viewType: string; readonly resource: URI; readonly backupId: string | undefined; + readonly canHotExit: boolean; isReadonly(): boolean | IMarkdownString; readonly onDidChangeReadonly: Event; diff --git a/code/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/code/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts index cb0defd952f..e4aa463c98d 100644 --- a/code/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/code/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -72,6 +72,10 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo return undefined; } + public get canHotExit() { + return true; // ensured via backups from text file models + } + public isDirty(): boolean { return this.textFileService.isDirty(this.resource); } diff --git a/code/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/code/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 90aba19a7f2..fe763dc159a 100644 --- a/code/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/code/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -7,8 +7,8 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { Codicon } from 'vs/base/common/codicons'; @@ -197,7 +197,7 @@ export abstract class AbstractExpressionsRenderer implements IT templateDisposable.add(setupCustomHover(getDefaultHoverDelegate('mouse'), lazyButton, localize('debug.lazyButton.tooltip', "Click to expand"))); const value = dom.append(expression, $('span.value')); - const label = new HighlightedLabel(name); + const label = templateDisposable.add(new HighlightedLabel(name)); const inputBoxContainer = dom.append(expression, $('.inputBoxContainer')); diff --git a/code/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/code/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1c36e83c55c..1b53d40047f 100644 --- a/code/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/code/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -54,7 +54,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = { description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), glyphMargin: { position: GlyphMarginLane.Right }, - glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint.")), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint")), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; @@ -327,7 +327,25 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi } } } else if (canSetBreakpoints) { - this.debugService.addBreakpoints(uri, [{ lineNumber }]); + if (e.event.middleButton) { + const action = this.configurationService.getValue('debug').gutterMiddleClickAction; + if (action !== 'none') { + let context: BreakpointWidgetContext; + switch (action) { + case 'logpoint': + context = BreakpointWidgetContext.LOG_MESSAGE; + break; + case 'conditionalBreakpoint': + context = BreakpointWidgetContext.CONDITION; + break; + case 'triggeredBreakpoint': + context = BreakpointWidgetContext.TRIGGER_POINT; + } + this.showBreakpointWidget(lineNumber, undefined, context); + } + } else { + this.debugService.addBreakpoints(uri, [{ lineNumber }]); + } } } })); diff --git a/code/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/code/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 5de52259df4..ddfb63625fd 100644 --- a/code/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/code/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -8,9 +8,9 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Gesture } from 'vs/base/browser/touch'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IListContextMenuEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -51,10 +51,11 @@ import { IEditorPane } from 'vs/workbench/common/editor'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DEBUG_SCHEME, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, DEBUG_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const $ = dom.$; @@ -87,6 +88,7 @@ export class BreakpointsView extends ViewPane { private ignoreLayout = false; private menu: IMenu; private breakpointItemType: IContextKey; + private breakpointIsDataBytes: IContextKey; private breakpointHasMultipleModes: IContextKey; private breakpointSupportsCondition: IContextKey; private _inputBoxData: InputBoxData | undefined; @@ -120,6 +122,7 @@ export class BreakpointsView extends ViewPane { this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); this._register(this.menu); this.breakpointItemType = CONTEXT_BREAKPOINT_ITEM_TYPE.bindTo(contextKeyService); + this.breakpointIsDataBytes = CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES.bindTo(contextKeyService); this.breakpointHasMultipleModes = CONTEXT_BREAKPOINT_HAS_MODES.bindTo(contextKeyService); this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); @@ -142,7 +145,7 @@ export class BreakpointsView extends ViewPane { new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), - this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType), + this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.breakpointIsDataBytes), new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), this.instantiationService.createInstance(InstructionBreakpointsRenderer), ], { @@ -266,6 +269,7 @@ export class BreakpointsView extends ViewPane { const session = this.debugService.getViewModel().focusedSession; const conditionSupported = element instanceof ExceptionBreakpoint ? element.supportsCondition : (!session || !!session.capabilities.supportsConditionalBreakpoints); this.breakpointSupportsCondition.set(conditionSupported); + this.breakpointIsDataBytes.set(element instanceof DataBreakpoint && element.src.type === DataBreakpointSetType.Address); const secondary: IAction[] = []; createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, { primary: [], secondary }, 'inline'); @@ -740,6 +744,7 @@ class DataBreakpointsRenderer implements IListRenderer, private breakpointSupportsCondition: IContextKey, private breakpointItemType: IContextKey, + private breakpointIsDataBytes: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -816,10 +821,12 @@ class DataBreakpointsRenderer implements IListRenderer 1); this.breakpointItemType.set('dataBreakpoint'); + this.breakpointIsDataBytes.set(dataBreakpoint.src.type === DataBreakpointSetType.Address); createAndFillInActionBarActions(this.menu, { arg: dataBreakpoint, shouldForwardArgs: true }, { primary, secondary: [] }, 'inline'); data.actionBar.clear(); data.actionBar.push(primary, { icon: true, label: false }); breakpointIdToActionBarDomeNode.set(dataBreakpoint.getId(), data.actionBar.domNode); + this.breakpointIsDataBytes.reset(); } disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { @@ -1421,6 +1428,166 @@ registerAction2(class extends Action2 { } }); +abstract class MemoryBreakpointAction extends Action2 { + async run(accessor: ServicesAccessor, existingBreakpoint?: IDataBreakpoint): Promise { + const debugService = accessor.get(IDebugService); + const session = debugService.getViewModel().focusedSession; + if (!session) { + return; + } + + let defaultValue = undefined; + if (existingBreakpoint && existingBreakpoint.src.type === DataBreakpointSetType.Address) { + defaultValue = `${existingBreakpoint.src.address} + ${existingBreakpoint.src.bytes}`; + } + + const quickInput = accessor.get(IQuickInputService); + const notifications = accessor.get(INotificationService); + const range = await this.getRange(quickInput, defaultValue); + if (!range) { + return; + } + + let info: IDataBreakpointInfoResponse | undefined; + try { + info = await session.dataBytesBreakpointInfo(range.address, range.bytes); + } catch (e) { + notifications.error(localize('dataBreakpointError', "Failed to set data breakpoint at {0}: {1}", range.address, e.message)); + } + + if (!info?.dataId) { + return; + } + + let accessType: DebugProtocol.DataBreakpointAccessType = 'write'; + if (info.accessTypes && info.accessTypes?.length > 1) { + const accessTypes = info.accessTypes.map(type => ({ label: type })); + const selectedAccessType = await quickInput.pick(accessTypes, { placeHolder: localize('dataBreakpointAccessType', "Select the access type to monitor") }); + if (!selectedAccessType) { + return; + } + + accessType = selectedAccessType.label; + } + + const src: DataBreakpointSource = { type: DataBreakpointSetType.Address, ...range }; + if (existingBreakpoint) { + await debugService.removeDataBreakpoints(existingBreakpoint.getId()); + } + + await debugService.addDataBreakpoint({ + description: info.description, + src, + canPersist: true, + accessTypes: info.accessTypes, + accessType: accessType, + initialSessionData: { session, dataId: info.dataId } + }); + } + + private getRange(quickInput: IQuickInputService, defaultValue?: string) { + return new Promise<{ address: string; bytes: number } | undefined>(resolve => { + const input = quickInput.createInputBox(); + input.prompt = localize('dataBreakpointMemoryRangePrompt', "Enter a memory range in which to break"); + input.placeholder = localize('dataBreakpointMemoryRangePlaceholder', 'Absolute range (0x1234 - 0x1300) or range of bytes after an address (0x1234 + 0xff)'); + if (defaultValue) { + input.value = defaultValue; + input.valueSelection = [0, defaultValue.length]; + } + input.onDidChangeValue(e => { + const err = this.parseAddress(e, false); + input.validationMessage = err?.error; + }); + input.onDidAccept(() => { + const r = this.parseAddress(input.value, true); + if ('error' in r) { + input.validationMessage = r.error; + } else { + resolve(r); + } + input.dispose(); + }); + input.onDidHide(() => { + resolve(undefined); + input.dispose(); + }); + input.ignoreFocusOut = true; + input.show(); + }); + } + + private parseAddress(range: string, isFinal: false): { error: string } | undefined; + private parseAddress(range: string, isFinal: true): { error: string } | { address: string; bytes: number }; + private parseAddress(range: string, isFinal: boolean): { error: string } | { address: string; bytes: number } | undefined { + const parts = /^(\S+)\s*(?:([+-])\s*(\S+))?/.exec(range); + if (!parts) { + return { error: localize('dataBreakpointAddrFormat', 'Address should be a range of numbers the form "[Start] - [End]" or "[Start] + [Bytes]"') }; + } + + const isNum = (e: string) => isFinal ? /^0x[0-9a-f]*|[0-9]*$/i.test(e) : /^0x[0-9a-f]+|[0-9]+$/i.test(e); + const [, startStr, sign = '+', endStr = '1'] = parts; + + for (const n of [startStr, endStr]) { + if (!isNum(n)) { + return { error: localize('dataBreakpointAddrStartEnd', 'Number must be a decimal integer or hex value starting with \"0x\", got {0}', n) }; + } + } + + if (!isFinal) { + return; + } + + const start = BigInt(startStr); + const end = BigInt(endStr); + const address = `0x${start.toString(16)}`; + if (sign === '-') { + return { address, bytes: Number(start - end) }; + } + + return { address, bytes: Number(end) }; + } +} + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.addDataBreakpointOnAddress', + title: { + ...localize2('addDataBreakpointOnAddress', "Add Data Breakpoint at Address"), + mnemonicTitle: localize({ key: 'miDataBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Data Breakpoint..."), + }, + f1: true, + icon: icons.watchExpressionsAddDataBreakpoint, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 11, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, ContextKeyExpr.equals('view', BREAKPOINTS_VIEW_ID)) + }, { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED + }] + }); + } +}); + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.editDataBreakpointOnAddress', + title: localize2('editDataBreakpointOnAddress', "Edit Address..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES), + group: 'navigation', + order: 15, + }] + }); + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/code/src/vs/workbench/contrib/debug/browser/callStackView.ts b/code/src/vs/workbench/contrib/debug/browser/callStackView.ts index 43b69137af3..db948476aef 100644 --- a/code/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/code/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -48,8 +48,8 @@ import { createDisconnectMenuItemAction } from 'vs/workbench/contrib/debug/brows import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, IStackFrame, IThread, State } from 'vs/workbench/contrib/debug/common/debug'; import { StackFrame, Thread, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = dom.$; @@ -348,6 +348,7 @@ export class CallStackView extends ViewPane { } if (!this.isBodyVisible()) { this.needsRefresh = true; + this.selectionNeedsUpdate = true; return; } if (this.onCallStackChangeScheduler.isScheduled()) { @@ -541,8 +542,8 @@ class SessionsRenderer implements ICompressibleTreeRenderer .title > .title-actions .action-label${ThemeIcon.asCSSSelector(icons.debugDisconnect)},.monaco-workbench .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor}; }`); + collector.addRule(`.monaco-workbench .part > .title > .title-actions .action-label${ThemeIcon.asCSSSelector(icons.debugDisconnect)},.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor}; }`); } const debugIconRestartColor = theme.getColor(debugIconRestartForeground); diff --git a/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 3bb555a1a53..f2f839ff912 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -237,7 +237,7 @@ async function goToBottomOfCallStack(debugService: IDebugService) { if (callStack.length > 0) { const nextVisibleFrame = findNextVisibleFrame(false, callStack, 0); // must consider the next frame up first, which will be the last frame if (nextVisibleFrame) { - debugService.focusStackFrame(nextVisibleFrame); + debugService.focusStackFrame(nextVisibleFrame, undefined, undefined, { preserveFocus: false }); } } } @@ -247,7 +247,7 @@ function goToTopOfCallStack(debugService: IDebugService) { const thread = debugService.getViewModel().focusedThread; if (thread) { - debugService.focusStackFrame(thread.getTopStackFrame()); + debugService.focusStackFrame(thread.getTopStackFrame(), undefined, undefined, { preserveFocus: false }); } } diff --git a/code/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/code/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index a800e241511..30789475e42 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -6,7 +6,7 @@ import { addDisposableListener, isKeyboardEvent } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { distinct, flatten } from 'vs/base/common/arrays'; +import { distinct } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; @@ -15,7 +15,7 @@ import { Event } from 'vs/base/common/event'; import { visit } from 'vs/base/common/json'; import { setProperty } from 'vs/base/common/jsonEdit'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, MutableDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; import { basename } from 'vs/base/common/path'; import * as env from 'vs/base/common/platform'; @@ -217,6 +217,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private altListener = new MutableDisposable(); private altPressed = false; private oldDecorations = this.editor.createDecorationsCollection(); + private displayedStore = new DisposableStore(); private editorHoverOptions: IEditorHoverOptions | undefined; private readonly debounceInfo: IFeatureDebounceInformation; @@ -237,7 +238,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { ) { this.debounceInfo = featureDebounceService.for(languageFeaturesService.inlineValuesProvider, 'InlineValues', { min: DEAFULT_INLINE_DEBOUNCE_DELAY }); this.hoverWidget = this.instantiationService.createInstance(DebugHoverWidget, this.editor); - this.toDispose = [this.defaultHoverLockout, this.altListener]; + this.toDispose = [this.defaultHoverLockout, this.altListener, this.displayedStore]; this.registerListeners(); this.exceptionWidgetVisible = CONTEXT_EXCEPTION_WIDGET_VISIBLE.bindTo(contextKeyService); this.toggleExceptionWidget(); @@ -639,7 +640,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private get removeInlineValuesScheduler(): RunOnceScheduler { return new RunOnceScheduler( () => { - this.oldDecorations.clear(); + this.displayedStore.clear(); }, 100 ); @@ -670,10 +671,14 @@ export class DebugEditorContribution implements IDebugEditorContribution { } this.removeInlineValuesScheduler.cancel(); + this.displayedStore.clear(); const viewRanges = this.editor.getVisibleRangesPlusViewportAboveBelow(); let allDecorations: IModelDeltaDecoration[]; + const cts = new CancellationTokenSource(); + this.displayedStore.add(toDisposable(() => cts.dispose(true))); + if (this.languageFeaturesService.inlineValuesProvider.has(model)) { const findVariable = async (_key: string, caseSensitiveLookup: boolean): Promise => { @@ -693,14 +698,13 @@ export class DebugEditorContribution implements IDebugEditorContribution { frameId: stackFrame.frameId, stoppedLocation: new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1, stackFrame.range.endLineNumber, stackFrame.range.endColumn + 1) }; - const token = new CancellationTokenSource().token; const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse(); allDecorations = []; const lineDecorations = new Map(); - const promises = flatten(providers.map(provider => viewRanges.map(range => Promise.resolve(provider.provideInlineValues(model, range, ctx, token)).then(async (result) => { + const promises = providers.flatMap(provider => viewRanges.map(range => Promise.resolve(provider.provideInlineValues(model, range, ctx, cts.token)).then(async (result) => { if (result) { for (const iv of result) { @@ -753,7 +757,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { } }, err => { onUnexpectedExternalError(err); - })))); + }))); const startTime = Date.now(); @@ -794,12 +798,15 @@ export class DebugEditorContribution implements IDebugEditorContribution { return createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value); })); - allDecorations = distinct(decorationsPerScope.reduce((previous, current) => previous.concat(current), []), + allDecorations = distinct(decorationsPerScope.flat(), // Deduplicate decorations since same variable can appear in multiple scopes, leading to duplicated decorations #129770 decoration => `${decoration.range.startLineNumber}:${decoration?.options.after?.content}`); } - this.oldDecorations.set(allDecorations); + if (!cts.token.isCancellationRequested) { + this.oldDecorations.set(allDecorations); + this.displayedStore.add(toDisposable(() => this.oldDecorations.clear())); + } } dispose(): void { @@ -810,8 +817,6 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.configurationWidget.dispose(); } this.toDispose = dispose(this.toDispose); - - this.oldDecorations.clear(); } } diff --git a/code/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/code/src/vs/workbench/contrib/debug/browser/debugIcons.ts index b1a9a4a0789..12376d1a83f 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -79,6 +79,7 @@ export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove- export const watchExpressionRemove = registerIcon('watch-expression-remove', Codicon.removeClose, localize('watchExpressionRemove', 'Icon for the Remove action in the watch view.')); export const watchExpressionsAdd = registerIcon('watch-expressions-add', Codicon.add, localize('watchExpressionsAdd', 'Icon for the add action in the watch view.')); export const watchExpressionsAddFuncBreakpoint = registerIcon('watch-expressions-add-function-breakpoint', Codicon.add, localize('watchExpressionsAddFuncBreakpoint', 'Icon for the add function breakpoint action in the watch view.')); +export const watchExpressionsAddDataBreakpoint = registerIcon('watch-expressions-add-data-breakpoint', Codicon.variableGroup, localize('watchExpressionsAddDataBreakpoint', 'Icon for the add data breakpoint action in the breakpoints view.')); export const breakpointsRemoveAll = registerIcon('breakpoints-remove-all', Codicon.closeAll, localize('breakpointsRemoveAll', 'Icon for the Remove All action in the breakpoints view.')); export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon.activateBreakpoints, localize('breakpointsActivate', 'Icon for the activate action in the breakpoints view.')); diff --git a/code/src/vs/workbench/contrib/debug/browser/debugService.ts b/code/src/vs/workbench/contrib/debug/browser/debugService.ts index 5478398dfb6..84e946ecf70 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -6,7 +6,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Action, IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { raceTimeout, RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isErrorWithActions } from 'vs/base/common/errorMessage'; import * as errors from 'vs/base/common/errors'; @@ -24,7 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -34,22 +34,21 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { EditorsOrder } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_HAS_DEBUGGED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, DEBUG_SCHEME, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; +import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; @@ -58,6 +57,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -1081,8 +1081,8 @@ export class DebugService implements IDebugService { await this.sendFunctionBreakpoints(); } - async addDataBreakpoint(description: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise { - this.model.addDataBreakpoint({ description, dataId, canPersist, accessTypes, accessType, mode }); + async addDataBreakpoint(opts: IDataBreakpointOptions): Promise { + this.model.addDataBreakpoint(opts); this.debugStorage.storeBreakpoints(this.model); await this.sendDataBreakpoints(); this.debugStorage.storeBreakpoints(this.model); diff --git a/code/src/vs/workbench/contrib/debug/browser/debugSession.ts b/code/src/vs/workbench/contrib/debug/browser/debugSession.ts index 861135c6f6a..0d56d661268 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -29,7 +29,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -41,6 +41,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isDefined } from 'vs/base/common/types'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -461,7 +462,7 @@ export class DebugSession implements IDebugSession, IDisposable { breakpoints: breakpointsToSend.map(bp => bp.toDAP()), sourceModified }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < breakpointsToSend.length; i++) { data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]); @@ -478,7 +479,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts.map(bp => bp.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < fbpts.length; i++) { data.set(fbpts[i].getId(), response.body.breakpoints[i]); @@ -506,7 +507,7 @@ export class DebugSession implements IDebugSession, IDisposable { } : { filters: exbpts.map(exb => exb.filter) }; const response = await this.raw.setExceptionBreakpoints(args); - if (response && response.body && response.body.breakpoints) { + if (response?.body && response.body.breakpoints) { const data = new Map(); for (let i = 0; i < exbpts.length; i++) { data.set(exbpts[i].getId(), response.body.breakpoints[i]); @@ -517,7 +518,19 @@ export class DebugSession implements IDebugSession, IDisposable { } } - async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + if (this.raw?.capabilities.supportsDataBreakpointBytes === false) { + throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes")); + } + + return this._dataBreakpointInfo({ name: address, bytes, asAddress: true }); + } + + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + return this._dataBreakpointInfo({ name, variablesReference }); + } + + private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } @@ -525,7 +538,7 @@ export class DebugSession implements IDebugSession, IDisposable { throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); } - const response = await this.raw.dataBreakpointInfo({ name, variablesReference }); + const response = await this.raw.dataBreakpointInfo(args); return response?.body; } @@ -535,11 +548,24 @@ export class DebugSession implements IDebugSession, IDisposable { } if (this.raw.readyForBreakpoints) { - const response = await this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints.map(bp => bp.toDAP()) }); - if (response && response.body) { + const converted = await Promise.all(dataBreakpoints.map(async bp => { + try { + const dap = await bp.toDAP(this); + return { dap, bp }; + } catch (e) { + return { bp, message: e.message }; + } + })); + const response = await this.raw.setDataBreakpoints({ breakpoints: converted.map(d => d.dap).filter(isDefined) }); + if (response?.body) { const data = new Map(); - for (let i = 0; i < dataBreakpoints.length; i++) { - data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]); + let i = 0; + for (const dap of converted) { + if (!dap.dap) { + data.set(dap.bp.getId(), dap.message); + } else if (i < response.body.breakpoints.length) { + data.set(dap.bp.getId(), response.body.breakpoints[i++]); + } } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } @@ -553,7 +579,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < instructionBreakpoints.length; i++) { data.set(instructionBreakpoints[i].getId(), response.body.breakpoints[i]); @@ -790,7 +816,7 @@ export class DebugSession implements IDebugSession, IDisposable { } const response = await this.raw.loadedSources({}); - if (response && response.body && response.body.sources) { + if (response?.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; @@ -959,7 +985,7 @@ export class DebugSession implements IDebugSession, IDisposable { private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise { if (this.raw) { const response = await this.raw.threads(); - if (response && response.body && response.body.threads) { + if (response?.body && response.body.threads) { this.model.rawUpdate({ sessionId: this.getId(), threads: response.body.threads, diff --git a/code/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/code/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 0b9fb05501d..edf29f96d3a 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -191,20 +191,19 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { const resizeListener = this._register(new MutableDisposable()); - this._register(this.layoutService.onDidChangeActiveContainer(() => { + this._register(this.layoutService.onDidChangeActiveContainer(async () => { this._yRange = undefined; - // note: we intentionally don't read the activeContainer before the - // `then` clause to avoid any races due to quickly switching windows. - this.layoutService.whenActiveContainerStylesLoaded.then(() => { - if (this.isBuilt) { - this.doShowInActiveContainer(); - this.setCoordinates(); - } + // note: we intentionally don't keep the activeContainer before the + // `await` clause to avoid any races due to quickly switching windows. + await this.layoutService.whenContainerStylesLoaded(dom.getWindow(this.layoutService.activeContainer)); + if (this.isBuilt) { + this.doShowInActiveContainer(); + this.setCoordinates(); + } - resizeListener.value = this._register(dom.addDisposableListener( - dom.getWindow(this.layoutService.activeContainer), dom.EventType.RESIZE, () => this.setYCoordinate())); - }); + resizeListener.value = this._register(dom.addDisposableListener( + dom.getWindow(this.layoutService.activeContainer), dom.EventType.RESIZE, () => this.setYCoordinate())); })); } diff --git a/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index e092df64537..92f139a3721 100644 --- a/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { $, Dimension, addStandardDisposableListener, append, getWindowById } from 'vs/base/browser/dom'; +import { $, Dimension, addStandardDisposableListener, append } from 'vs/base/browser/dom'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { binarySearch2 } from 'vs/base/common/arrays'; @@ -42,6 +42,7 @@ import { InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugMo import { getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { isUri, sourcesEqual } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; interface IDisassembledInstructionEntry { allowBreakpoint: boolean; @@ -92,6 +93,7 @@ export class DisassemblyView extends EditorPane { private readonly _referenceToMemoryAddress = new Map(); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -99,7 +101,7 @@ export class DisassemblyView extends EditorPane { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDebugService private readonly _debugService: IDebugService, ) { - super(DISASSEMBLY_VIEW_ID, telemetryService, themeService, storageService); + super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService); this._disassembledInstructions = undefined; this._onDidChangeStackFrame = this._register(new Emitter({ leakWarningThreshold: 1000 })); @@ -133,8 +135,7 @@ export class DisassemblyView extends EditorPane { } private createFontInfo() { - const window = getWindowById(this.group?.windowId, true).window; - return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(window).value); + return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value); } get currentInstructionAddresses() { diff --git a/code/src/vs/workbench/contrib/debug/browser/replViewer.ts b/code/src/vs/workbench/contrib/debug/browser/replViewer.ts index 64c3f3d6ccd..c227a6d18f5 100644 --- a/code/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/code/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -28,8 +28,8 @@ import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpres import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplOutputElement, ReplVariableElement } from 'vs/workbench/contrib/debug/common/replModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = dom.$; @@ -84,7 +84,7 @@ export class ReplEvaluationInputsRenderer implements ITreeRenderer(resolve => { - let installing = false; - - const handle = notifications.prompt( - Severity.Info, - localize("viewMemory.prompt", "Inspecting binary data requires the Hex Editor extension. Would you like to install it now?"), [ - { - label: localize("cancel", "Cancel"), - run: () => resolve(false), - }, - { - label: localize("install", "Install"), - run: async () => { - installing = true; - try { - await progressService.withProgress( - { - location: ProgressLocation.Notification, - title: localize("viewMemory.install.progress", "Installing the Hex Editor..."), - }, - async () => { - await commandService.executeCommand('workbench.extensions.installExtension', HEX_EDITOR_EXTENSION_ID); - // it seems like the extension is not registered immediately on install -- - // wait for it to appear before returning. - while (!(await extensionService.getExtension(HEX_EDITOR_EXTENSION_ID))) { - await timeout(30); - } - }, - ); - resolve(true); - } catch (e) { - notifications.error(e as Error); - resolve(false); - } - } - }, - ], - { sticky: true }, - ); - - handle.onDidClose(e => { - if (!installing) { - resolve(false); - } - }); - }); +async function tryInstallHexEditor(extensionsWorkbenchService: IExtensionsWorkbenchService, notificationService: INotificationService): Promise { + try { + await extensionsWorkbenchService.install(HEX_EDITOR_EXTENSION_ID, { + justification: localize("viewMemory.prompt", "Inspecting binary data requires this extension."), + enable: true + }, ProgressLocation.Notification); + return true; + } catch (error) { + notificationService.error(error); + return false; + } } export const BREAK_WHEN_VALUE_CHANGES_ID = 'debug.breakWhenValueChanges'; @@ -802,7 +766,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'write', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'write' }); } } }); @@ -813,7 +777,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'readWrite', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'readWrite' }); } } }); @@ -824,7 +788,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'read', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'read' }); } } }); diff --git a/code/src/vs/workbench/contrib/debug/common/debug.ts b/code/src/vs/workbench/contrib/debug/common/debug.ts index 86d0e94b826..2820f075163 100644 --- a/code/src/vs/workbench/contrib/debug/common/debug.ts +++ b/code/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,7 +24,7 @@ import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDataBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -62,6 +62,7 @@ export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); +export const CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES = new RawContextKey('breakpointItemBytes', undefined, { type: 'boolean', description: nls.localize('breakpointItemIsDataBytes', "Whether the breakpoint item is a data breakpoint on a byte range.") }); export const CONTEXT_BREAKPOINT_HAS_MODES = new RawContextKey('breakpointHasModes', false, { type: 'boolean', description: nls.localize('breakpointHasModes', "Whether the breakpoint has multiple modes it can switch to.") }); export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false, { type: 'boolean', description: nls.localize('breakpointSupportsCondition', "True when the focused breakpoint supports conditions.") }); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false, { type: 'boolean', description: nls.localize('loadedScriptsSupported', "True when the focused sessions supports the LOADED SCRIPTS view") }); @@ -78,6 +79,7 @@ export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggers export const CONTEXT_DEBUG_EXTENSION_AVAILABLE = new RawContextKey('debugExtensionAvailable', true, { type: 'boolean', description: nls.localize('debugExtensionsAvailable', "True when there is at least one debug extension installed and enabled.") }); export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey('debugProtocolVariableMenuContext', undefined, { type: 'string', description: nls.localize('debugProtocolVariableMenuContext', "Represents the context the debug adapter sets on the focused variable in the VARIABLES view.") }); export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugSetVariableSupported', false, { type: 'boolean', description: nls.localize('debugSetVariableSupported', "True when the focused session supports 'setVariable' request.") }); +export const CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED = new RawContextKey('debugSetDataBreakpointAddressSupported', false, { type: 'boolean', description: nls.localize('debugSetDataBreakpointAddressSupported', "True when the focused session supports 'getBreakpointInfo' request on an address.") }); export const CONTEXT_SET_EXPRESSION_SUPPORTED = new RawContextKey('debugSetExpressionSupported', false, { type: 'boolean', description: nls.localize('debugSetExpressionSupported', "True when the focused session supports 'setExpression' request.") }); export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueChangesSupported', "True when the focused session supports to break when value changes.") }); export const CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED = new RawContextKey('breakWhenValueIsAccessedSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsAccessedSupported', "True when the focused breakpoint supports to break when value is accessed.") }); @@ -404,6 +406,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; dataBreakpointInfo(name: string, variablesReference?: number): Promise; + dataBytesBreakpointInfo(address: string, bytes: number): Promise; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendInstructionBreakpoints(dbps: IInstructionBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; @@ -607,12 +610,26 @@ export interface IExceptionBreakpoint extends IBaseBreakpoint { readonly description: string | undefined; } +export const enum DataBreakpointSetType { + Variable, + Address, +} + +/** + * Source for a data breakpoint. A data breakpoint on a variable always has a + * `dataId` because it cannot reference that variable globally, but addresses + * can request info repeated and use session-specific data. + */ +export type DataBreakpointSource = + | { type: DataBreakpointSetType.Variable; dataId: string } + | { type: DataBreakpointSetType.Address; address: string; bytes: number }; + export interface IDataBreakpoint extends IBaseBreakpoint { readonly description: string; - readonly dataId: string; readonly canPersist: boolean; + readonly src: DataBreakpointSource; readonly accessType: DebugProtocol.DataBreakpointAccessType; - toDAP(): DebugProtocol.DataBreakpoint; + toDAP(session: IDebugSession): Promise; } export interface IInstructionBreakpoint extends IBaseBreakpoint { @@ -720,6 +737,7 @@ export interface IBreakpointsChangeEvent { export interface IDebugConfiguration { allowBreakpointsEverywhere: boolean; + gutterMiddleClickAction: 'logpoint' | 'conditionalBreakpoint' | 'triggeredBreakpoint' | 'none'; openDebug: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak'; openExplorerOnEnd: boolean; inlineValues: boolean | 'auto' | 'on' | 'off'; // boolean for back-compat @@ -1144,7 +1162,7 @@ export interface IDebugService { /** * Adds a new data breakpoint. */ - addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise; + addDataBreakpoint(opts: IDataBreakpointOptions): Promise; /** * Updates an already existing data breakpoint. diff --git a/code/src/vs/workbench/contrib/debug/common/debugModel.ts b/code/src/vs/workbench/contrib/debug/common/debugModel.ts index 8098d1ce57b..b12e9b726ff 100644 --- a/code/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/code/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -1150,15 +1150,18 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export interface IDataBreakpointOptions extends IBaseBreakpointOptions { description: string; - dataId: string; + src: DataBreakpointSource; canPersist: boolean; + initialSessionData?: { session: IDebugSession; dataId: string }; accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; accessType: DebugProtocol.DataBreakpointAccessType; } export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { + private readonly sessionDataIdForAddr = new WeakMap(); + public readonly description: string; - public readonly dataId: string; + public readonly src: DataBreakpointSource; public readonly canPersist: boolean; public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; public readonly accessType: DebugProtocol.DataBreakpointAccessType; @@ -1169,15 +1172,36 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { ) { super(id, opts); this.description = opts.description; - this.dataId = opts.dataId; + if ('dataId' in opts) { // back compat with old saved variables in 1.87 + opts.src = { type: DataBreakpointSetType.Variable, dataId: opts.dataId as string }; + } + this.src = opts.src; this.canPersist = opts.canPersist; this.accessTypes = opts.accessTypes; this.accessType = opts.accessType; + if (opts.initialSessionData) { + this.sessionDataIdForAddr.set(opts.initialSessionData.session, opts.initialSessionData.dataId); + } } - toDAP(): DebugProtocol.DataBreakpoint { + async toDAP(session: IDebugSession): Promise { + let dataId: string; + if (this.src.type === DataBreakpointSetType.Variable) { + dataId = this.src.dataId; + } else { + let sessionDataId = this.sessionDataIdForAddr.get(session); + if (!sessionDataId) { + sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; + if (!sessionDataId) { + return undefined; + } + this.sessionDataIdForAddr.set(session, sessionDataId); + } + dataId = sessionDataId; + } + return { - dataId: this.dataId, + dataId, accessType: this.accessType, condition: this.condition, hitCondition: this.hitCondition, @@ -1188,7 +1212,7 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { return { ...super.toJSON(), description: this.description, - dataId: this.dataId, + src: this.src, accessTypes: this.accessTypes, accessType: this.accessType, canPersist: this.canPersist, diff --git a/code/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/code/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index b00a4fd466a..50eacfd65e2 100644 --- a/code/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/code/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -813,11 +813,22 @@ declare module DebugProtocol { /** Reference to the variable container if the data breakpoint is requested for a child of the container. The `variablesReference` must have been obtained in the current suspended state. See 'Lifetime of Object References' in the Overview section for details. */ variablesReference?: number; /** The name of the variable's child to obtain data breakpoint information for. - If `variablesReference` isn't specified, this can be an expression. + If `variablesReference` isn't specified, this can be an expression, or an address if `asAddress` is also true. */ name: string; /** When `name` is an expression, evaluate it in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. When `variablesReference` is specified, this property has no effect. */ frameId?: number; + /** If specified, a debug adapter should return information for the range of memory extending `bytes` number of bytes from the address or variable specified by `name`. Breakpoints set using the resulting data ID should pause on data access anywhere within that range. + + Clients may set this property only if the `supportsDataBreakpointBytes` capability is true. + */ + bytes?: number; + /** If `true`, the `name` is a memory address and the debugger should interpret it as a decimal value, or hex value if it is prefixed with `0x`. + + Clients may set this property only if the `supportsDataBreakpointBytes` + capability is true. + */ + asAddress?: boolean; /** The mode of the desired breakpoint. If defined, this must be one of the `breakpointModes` the debug adapter advertised in its `Capabilities`. */ mode?: string; } @@ -1680,42 +1691,6 @@ declare module DebugProtocol { }; } - /** DataAddressBreakpointInfo request; value of command field is 'DataAddressBreakpointInfo'. - Obtains information on a possible data breakpoint that could be set on a memory address or memory address range. - - Clients should only call this request if the corresponding capability `supportsDataAddressInfo` is true. - */ - interface DataAddressBreakpointInfoRequest extends Request { - // command: 'DataAddressBreakpointInfo'; - arguments: DataAddressBreakpointInfoArguments; - } - - /** Arguments for `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoArguments { - /** The address of the data for which to obtain breakpoint information. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - address?: string; - /** If passed, requests breakpoint information for an exclusive byte range rather than a single address. The range extends the given number of `bytes` from the start `address`. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - bytes?: string; - } - - /** Response to `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoResponse extends Response { - body: { - /** An identifier for the data on which a data breakpoint can be registered with the `setDataBreakpoints` request or null if no data breakpoint is available. If a `variablesReference` or `frameId` is passed, the `dataId` is valid in the current suspended state, otherwise it's valid indefinitely. See 'Lifetime of Object References' in the Overview section for details. Breakpoints set using the `dataId` in the `setDataBreakpoints` request may outlive the lifetime of the associated `dataId`. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Attribute lists the available access types for a potential data breakpoint. A UI client could surface this information. */ - accessTypes?: DataBreakpointAccessType[]; - /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - }; - } - /** Information about the capabilities of a debug adapter. */ interface Capabilities { /** The debug adapter supports the `configurationDone` request. */ @@ -1788,8 +1763,6 @@ declare module DebugProtocol { supportsBreakpointLocationsRequest?: boolean; /** The debug adapter supports the `clipboard` context value in the `evaluate` request. */ supportsClipboardContext?: boolean; - /** The debug adapter supports the `dataAddressBreakpointInfo` request. */ - supportsDataAddressInfo?: boolean; /** The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. */ supportsSteppingGranularity?: boolean; /** The debug adapter supports adding breakpoints based on instruction references. */ @@ -1798,6 +1771,8 @@ declare module DebugProtocol { supportsExceptionFilterOptions?: boolean; /** The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). */ supportsSingleThreadExecutionRequests?: boolean; + /** The debug adapter supports the `asAddress` and `bytes` fields in the `dataBreakpointInfo` request. */ + supportsDataBreakpointBytes?: boolean; /** Modes of breakpoints supported by the debug adapter, such as 'hardware' or 'software'. If present, the client may allow the user to select a mode and include it in its `setBreakpoints` request. Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. diff --git a/code/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/code/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 4b0959a97a8..7221f390771 100644 --- a/code/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/code/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { @@ -34,6 +34,7 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private setDataBreakpointAtByteSupported!: IContextKey; private setExpressionSupported!: IContextKey; private multiSessionDebug!: IContextKey; private terminateDebuggeeSupported!: IContextKey; @@ -52,6 +53,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.setDataBreakpointAtByteSupported = CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED.bindTo(contextKeyService); this.setExpressionSupported = CONTEXT_SET_EXPRESSION_SUPPORTED.bindTo(contextKeyService); this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); this.terminateDebuggeeSupported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); @@ -88,15 +90,16 @@ export class ViewModel implements IViewModel { this._focusedSession = session; this.contextKeyService.bufferChangeEvents(() => { - this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false); - this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false); - this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); - this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false); - this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); - this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); - this.setExpressionSupported.set(session ? !!session.capabilities.supportsSetExpression : false); - this.terminateDebuggeeSupported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); - this.suspendDebuggeeSupported.set(session ? !!session.capabilities.supportSuspendDebuggee : false); + this.loadedScriptsSupportedContextKey.set(!!session?.capabilities.supportsLoadedSourcesRequest); + this.stepBackSupportedContextKey.set(!!session?.capabilities.supportsStepBack); + this.restartFrameSupportedContextKey.set(!!session?.capabilities.supportsRestartFrame); + this.stepIntoTargetsSupported.set(!!session?.capabilities.supportsStepInTargetsRequest); + this.jumpToCursorSupported.set(!!session?.capabilities.supportsGotoTargetsRequest); + this.setVariableSupported.set(!!session?.capabilities.supportsSetVariable); + this.setDataBreakpointAtByteSupported.set(!!session?.capabilities.supportsDataBreakpointBytes); + this.setExpressionSupported.set(!!session?.capabilities.supportsSetExpression); + this.terminateDebuggeeSupported.set(!!session?.capabilities.supportTerminateDebuggee); + this.suspendDebuggeeSupported.set(!!session?.capabilities.supportSuspendDebuggee); this.disassembleRequestSupported.set(!!session?.capabilities.supportsDisassembleRequest); this.focusedStackFrameHasInstructionPointerReference.set(!!stackFrame?.instructionPointerReference); const attach = !!session && isSessionAttach(session); diff --git a/code/src/vs/workbench/contrib/debug/node/terminals.ts b/code/src/vs/workbench/contrib/debug/node/terminals.ts index c3f3cd92928..84c3d7947d4 100644 --- a/code/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/code/src/vs/workbench/contrib/debug/node/terminals.ts @@ -56,7 +56,7 @@ export async function hasChildProcesses(processId: number | undefined): Promise< const enum ShellType { cmd, powershell, bash } -export function prepareCommand(shell: string, args: string[], argsCanBeInterpretedByShell: boolean, cwd?: string, env?: { [key: string]: string | null }): string { +export function prepareCommand(shell: string, args: string[], argsCanBeInterpretedByShell: boolean, cwd?: string): string { shell = shell.trim().toLowerCase(); @@ -97,16 +97,6 @@ export function prepareCommand(shell: string, args: string[], argsCanBeInterpret } command += `cd ${quote(cwd)}; `; } - if (env) { - for (const key in env) { - const value = env[key]; - if (value === null) { - command += `Remove-Item env:${key}; `; - } else { - command += `\${env:${key}}='${value}'; `; - } - } - } if (args.length > 0) { const arg = args.shift()!; const cmd = argsCanBeInterpretedByShell ? arg : quote(arg); @@ -137,25 +127,10 @@ export function prepareCommand(shell: string, args: string[], argsCanBeInterpret } command += `cd ${quote(cwd)} && `; } - if (env) { - command += 'cmd /C "'; - for (const key in env) { - let value = env[key]; - if (value === null) { - command += `set "${key}=" && `; - } else { - value = value.replace(/[&^|<>]/g, s => `^${s}`); - command += `set "${key}=${value}" && `; - } - } - } for (const a of args) { command += (a === '<' || a === '>' || argsCanBeInterpretedByShell) ? a : quote(a); command += ' '; } - if (env) { - command += '"'; - } break; case ShellType.bash: { @@ -165,25 +140,9 @@ export function prepareCommand(shell: string, args: string[], argsCanBeInterpret return s.length === 0 ? `""` : s; }; - const hardQuote = (s: string) => { - return /[^\w@%\/+=,.:^-]/.test(s) ? `'${s.replace(/'/g, '\'\\\'\'')}'` : s; - }; - if (cwd) { command += `cd ${quote(cwd)} ; `; } - if (env) { - command += '/usr/bin/env'; - for (const key in env) { - const value = env[key]; - if (value === null) { - command += ` -u ${hardQuote(key)}`; - } else { - command += ` ${hardQuote(`${key}=${value}`)}`; - } - } - command += ' '; - } for (const a of args) { command += (a === '<' || a === '>' || argsCanBeInterpretedByShell) ? a : quote(a); command += ' '; diff --git a/code/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/code/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 43c4f7d0b11..173efc27ee0 100644 --- a/code/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/code/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -105,7 +105,6 @@ suite('Debug - Base Debug View', () => { renderVariable(variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); assert.strictEqual(value.textContent, 'hey'); assert.strictEqual(label.element.textContent, 'foo:'); - assert.strictEqual(label.element.title, 'string'); variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; expression = $('.'); @@ -122,8 +121,9 @@ suite('Debug - Base Debug View', () => { renderVariable(variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); assert.strictEqual(name.className, 'virtual'); assert.strictEqual(label.element.textContent, 'console:'); - assert.strictEqual(label.element.title, 'console'); assert.strictEqual(value.className, 'value number'); + + label.dispose(); }); test('statusbar in debug mode', () => { diff --git a/code/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/code/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index b85e544f9bb..61599c36ce9 100644 --- a/code/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/code/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -19,7 +19,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { getBreakpointMessageAndIcon, getExpandedBodySize } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DataBreakpointSetType, IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; @@ -313,13 +313,13 @@ suite('Debug - Breakpoints', () => { let eventCount = 0; disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); - model.addDataBreakpoint({ description: 'label', dataId: 'id', canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); - model.addDataBreakpoint({ description: 'second', dataId: 'secondId', canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); + model.addDataBreakpoint({ description: 'label', src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); + model.addDataBreakpoint({ description: 'second', src: { type: DataBreakpointSetType.Variable, dataId: 'secondId' }, canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); model.updateDataBreakpoint('1', { condition: 'aCondition' }); model.updateDataBreakpoint('2', { hitCondition: '10' }); const dataBreakpoints = model.getDataBreakpoints(); assert.strictEqual(dataBreakpoints[0].canPersist, true); - assert.strictEqual(dataBreakpoints[0].dataId, 'id'); + assert.deepStrictEqual(dataBreakpoints[0].src, { type: DataBreakpointSetType.Variable, dataId: 'id' }); assert.strictEqual(dataBreakpoints[0].accessType, 'read'); assert.strictEqual(dataBreakpoints[0].condition, 'aCondition'); assert.strictEqual(dataBreakpoints[1].canPersist, false); @@ -374,7 +374,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Disabled Logpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log-disabled'); - model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', dataId: 'id' }); + model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', src: { type: DataBreakpointSetType.Variable, dataId: 'id' } }); const dataBreakpoints = model.getDataBreakpoints(); result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0], ls, model); assert.strictEqual(result.message, 'Data Breakpoint'); diff --git a/code/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/code/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 617f46d449f..464a4794def 100644 --- a/code/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/code/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -13,7 +13,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -114,7 +114,7 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { + addDataBreakpoint(): Promise { throw new Error('Method not implemented.'); } @@ -223,6 +223,10 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + throw new Error('Method not implemented.'); + } + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined } | undefined> { throw new Error('Method not implemented.'); } diff --git a/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 5baf0bb721d..000198bfdd4 100644 --- a/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -910,7 +910,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo if (!this.registeredCommands.has(command.id)) { this.registeredCommands.add(command.id); - registerAction2(class StandaloneContinueOnOption extends Action2 { + this._register(registerAction2(class StandaloneContinueOnOption extends Action2 { constructor() { super(command); } @@ -918,7 +918,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo async run(accessor: ServicesAccessor): Promise { return accessor.get(ICommandService).executeCommand(continueWorkingOnCommand.id, undefined, commandId); } - }); + })); if (remoteGroup !== undefined) { MenuRegistry.appendMenuItem(MenuId.StatusBarRemoteIndicatorMenu, { diff --git a/code/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/code/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 6ba3511f0ee..f612c22c3e3 100644 --- a/code/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/code/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -346,8 +346,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + if (!signedInForProvider || this.authenticationService.getProvider(authenticationProvider.id).supportsMultipleAccounts) { + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider }); } } @@ -370,7 +370,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const session of sessions) { const item = { label: session.account.label, - description: this.authenticationService.getLabel(provider.id), + description: this.authenticationService.getProvider(provider.id).label, session: { ...session, providerId: provider.id } }; accounts.set(item.session.account.id, item); diff --git a/code/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts b/code/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts index 5793dde0157..ce4af2c979b 100644 --- a/code/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts +++ b/code/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts @@ -67,7 +67,7 @@ export class EditSessionsDataViews extends Disposable { order: 1 }); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.editSessions.actions.resume', @@ -87,9 +87,9 @@ export class EditSessionsDataViews extends Disposable { await commandService.executeCommand('workbench.editSessions.actions.resumeLatest', editSessionId, true); await treeView.refresh(); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.editSessions.actions.store', @@ -103,9 +103,9 @@ export class EditSessionsDataViews extends Disposable { await commandService.executeCommand('workbench.editSessions.actions.storeCurrent'); await treeView.refresh(); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.editSessions.actions.delete', @@ -134,9 +134,9 @@ export class EditSessionsDataViews extends Disposable { await treeView.refresh(); } } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.editSessions.actions.deleteAll', @@ -163,7 +163,7 @@ export class EditSessionsDataViews extends Disposable { await treeView.refresh(); } } - }); + })); } } diff --git a/code/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/code/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index cb2c01bdf75..7c7fbac3a95 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -5,8 +5,8 @@ import { $, Dimension, addDisposableListener, append, clearNode } from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -38,6 +38,7 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { errorIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; @@ -77,6 +78,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { private _updateSoon: RunOnceScheduler; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -91,7 +93,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { @IClipboardService private readonly _clipboardService: IClipboardService, @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(AbstractRuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); + super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService); this._list = null; this._elements = null; diff --git a/code/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index 775e2254338..28ae3c3e3ed 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -73,7 +73,7 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { private toExtensionRecommendation(tip: IConfigBasedExtensionTip): ConfigBasedExtensionRecommendation { return { - extensionId: tip.extensionId, + extension: tip.extensionId, reason: { reasonId: ExtensionRecommendationReason.WorkspaceConfig, reasonText: localize('exeBasedRecommendation', "This extension is recommended because of the current workspace configuration") diff --git a/code/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts b/code/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts index 2345dedabd3..6b85011360d 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts @@ -14,12 +14,14 @@ import { distinct } from 'vs/base/common/arrays'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; export class DeprecatedExtensionsChecker extends Disposable implements IWorkbenchContribution { constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -45,7 +47,7 @@ export class DeprecatedExtensionsChecker extends Disposable implements IWorkbenc } const local = await this.extensionsWorkbenchService.queryLocal(); const previouslyNotified = this.getNotifiedDeprecatedExtensions(); - const toNotify = local.filter(e => !!e.deprecationInfo).filter(e => !previouslyNotified.includes(e.identifier.id.toLowerCase())); + const toNotify = local.filter(e => !!e.deprecationInfo && e.local && this.extensionEnablementService.isEnabled(e.local)).filter(e => !previouslyNotified.includes(e.identifier.id.toLowerCase())); if (toNotify.length) { this.notificationService.prompt( Severity.Warning, diff --git a/code/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 6096e57e82a..9e75f3fb4a9 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -58,7 +58,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { private toExtensionRecommendation(tip: IExecutableBasedExtensionTip): ExtensionRecommendation { return { - extensionId: tip.extensionId.toLowerCase(), + extension: tip.extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Executable, reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", tip.exeFriendlyName) diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 11ee0d9a2b9..26de19cb281 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -5,8 +5,8 @@ import { $, Dimension, addDisposableListener, append, setParentFlowTo } from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { CheckboxActionViewItem } from 'vs/base/browser/ui/toggle/toggle'; import { Action, IAction } from 'vs/base/common/actions'; @@ -44,6 +44,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { buttonForeground, buttonHoverBackground, editorBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ViewContainerLocation } from 'vs/workbench/common/views'; @@ -61,7 +62,7 @@ import { InstallDropdownAction, InstallingLabelAction, LocalInstallAction, MigrateDeprecatedExtensionAction, - ReloadAction, + ExtensionRuntimeStateAction, RemoteInstallAction, SetColorThemeAction, SetFileIconThemeAction, @@ -78,13 +79,18 @@ import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, VerifiedPublisherWidget, onClick } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; +import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { VIEW_ID as EXPLORER_VIEW_ID } from 'vs/workbench/contrib/files/common/files'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; class NavBar extends Disposable { @@ -154,6 +160,7 @@ interface IExtensionEditorTemplate { builtin: HTMLElement; publisher: HTMLElement; publisherDisplayName: HTMLElement; + resource: HTMLElement; installCount: HTMLElement; rating: HTMLElement; description: HTMLElement; @@ -228,6 +235,7 @@ export class ExtensionEditor extends EditorPane { private showPreReleaseVersionContextKey: IContextKey | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -243,8 +251,12 @@ export class ExtensionEditor extends EditorPane { @ILanguageService private readonly languageService: ILanguageService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IExplorerService private readonly explorerService: IExplorerService, + @IViewsService private readonly viewsService: IViewsService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { - super(ExtensionEditor.ID, telemetryService, themeService, storageService); + super(ExtensionEditor.ID, group, telemetryService, themeService, storageService); this.extensionReadme = null; this.extensionChangelog = null; this.extensionManifest = null; @@ -289,6 +301,9 @@ export class ExtensionEditor extends EditorPane { const publisherDisplayName = append(publisher, $('.publisher-name')); const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $('.verified-publisher')), false); + const resource = append(append(subtitle, $('.subtitle-entry.resource')), $('', { tabIndex: 0 })); + resource.setAttribute('role', 'button'); + const installCount = append(append(subtitle, $('.subtitle-entry')), $('span.install', { tabIndex: 0 })); this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), installCount, localize('install count', "Install count"))); const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCount, false); @@ -313,7 +328,7 @@ export class ExtensionEditor extends EditorPane { const installAction = this.instantiationService.createInstance(InstallDropdownAction); const actions = [ - this.instantiationService.createInstance(ReloadAction), + this.instantiationService.createInstance(ExtensionRuntimeStateAction), this.instantiationService.createInstance(ExtensionStatusLabelAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', [[this.instantiationService.createInstance(UpdateAction, true)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), @@ -417,6 +432,7 @@ export class ExtensionEditor extends EditorPane { preview, publisher, publisherDisplayName, + resource, rating, actionsAndStatusContainer, extensionActionBar, @@ -471,6 +487,12 @@ export class ExtensionEditor extends EditorPane { } private async getGalleryVersionToShow(extension: IExtension, preRelease?: boolean): Promise { + if (extension.resourceExtension) { + return null; + } + if (extension.local?.source === 'resource') { + return null; + } if (isUndefined(preRelease)) { return null; } @@ -519,6 +541,25 @@ export class ExtensionEditor extends EditorPane { // subtitle template.publisher.classList.toggle('clickable', !!extension.url); template.publisherDisplayName.textContent = extension.publisherDisplayName; + template.publisher.parentElement?.classList.toggle('hide', !!extension.resourceExtension || extension.local?.source === 'resource'); + + const location = extension.resourceExtension?.location ?? (extension.local?.source === 'resource' ? extension.local?.location : undefined); + template.resource.parentElement?.classList.toggle('hide', !location); + if (location) { + const workspaceFolder = this.contextService.getWorkspaceFolder(location); + if (workspaceFolder && extension.isWorkspaceScoped) { + template.resource.parentElement?.classList.add('clickable'); + this.transientDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); + template.resource.textContent = localize('workspace extension', "Workspace Extension"); + this.transientDisposables.add(onClick(template.resource, () => { + this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); + })); + } else { + template.resource.parentElement?.classList.remove('clickable'); + this.transientDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); + template.resource.textContent = localize('local extension', "Local Extension"); + } + } template.installCount.parentElement?.classList.toggle('hide', !extension.url); template.rating.parentElement?.classList.toggle('hide', !extension.url); @@ -674,14 +715,14 @@ export class ExtensionEditor extends EditorPane { webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0; - webview.claim(this, this.scopedContextKeyService); + webview.claim(this, this.window, this.scopedContextKeyService); setParentFlowTo(webview.container, container); webview.layoutWebviewOverElement(container); webview.setHtml(body); - webview.claim(this, undefined); + webview.claim(this, this.window, undefined); - this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus())); + this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire())); this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts index 8bd7b3cd177..a679ef876b4 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { $, append, clearNode } from 'vs/base/browser/dom'; import { Emitter, Event } from 'vs/base/common/event'; import { ExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; @@ -447,16 +447,18 @@ class ExtensionFeatureView extends Disposable { private renderTableData(container: HTMLElement, renderer: IExtensionFeatureTableRenderer): void { const tableData = this._register(renderer.render(this.manifest)); + const tableDisposable = this._register(new MutableDisposable()); if (tableData.onDidChange) { this._register(tableData.onDidChange(data => { clearNode(container); - this.renderTable(data, container); + tableDisposable.value = this.renderTable(data, container); })); } - this.renderTable(tableData.data, container); + tableDisposable.value = this.renderTable(tableData.data, container); } - private renderTable(tableData: ITableData, container: HTMLElement): void { + private renderTable(tableData: ITableData, container: HTMLElement): IDisposable { + const disposables = new DisposableStore(); append(container, $('table', undefined, $('tr', undefined, @@ -478,7 +480,7 @@ class ExtensionFeatureView extends Disposable { result.push(element); } else if (item instanceof ResolvedKeybinding) { const element = $(''); - const kbl = new KeybindingLabel(element, OS, defaultKeybindingLabelStyles); + const kbl = disposables.add(new KeybindingLabel(element, OS, defaultKeybindingLabelStyles)); kbl.set(item); result.push(element); } else if (item instanceof Color) { @@ -490,6 +492,7 @@ class ExtensionFeatureView extends Disposable { }) ); }))); + return disposables; } private renderMarkdownData(container: HTMLElement, renderer: IExtensionFeatureMarkdownRenderer): void { diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index d68f705d95f..6dd9ecd1fe1 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -10,6 +10,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, isDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isString } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -18,6 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -49,6 +52,8 @@ type RecommendationsNotificationActions = { onDidNeverShowRecommendedExtensionsAgain(extensions: IExtension[]): void; }; +type ExtensionRecommendations = Omit & { extensions: Array }; + class RecommendationsNotification extends Disposable { private _onDidClose = this._register(new Emitter()); @@ -139,6 +144,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); } @@ -183,14 +189,16 @@ export class ExtensionRecommendationNotificationService extends Disposable imple }); } - async promptWorkspaceRecommendations(recommendations: string[]): Promise { + async promptWorkspaceRecommendations(recommendations: Array): Promise { if (this.storageService.getBoolean(donotShowWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false)) { return; } let installed = await this.extensionManagementService.getInstalled(); installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind - recommendations = recommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + recommendations = recommendations.filter(recommendation => installed.every(local => + isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.location) + )); if (!recommendations.length) { return; } @@ -207,7 +215,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple } - private async promptRecommendationsNotification({ extensions: extensionIds, source, name, searchValue }: IExtensionRecommendations, recommendationsNotificationActions: RecommendationsNotificationActions): Promise { + private async promptRecommendationsNotification({ extensions: extensionIds, source, name, searchValue }: ExtensionRecommendations, recommendationsNotificationActions: RecommendationsNotificationActions): Promise { if (this.hasToIgnoreRecommendationNotifications()) { return RecommendationsNotificationResult.Ignored; @@ -228,7 +236,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple this.recommendationSources.push(source); // Ignore exe recommendation if recommendations are already shown - if (source === RecommendationSource.EXE && extensionIds.every(id => this.recommendedExtensions.includes(id))) { + if (source === RecommendationSource.EXE && extensionIds.every(id => isString(id) && this.recommendedExtensions.includes(id))) { return RecommendationsNotificationResult.Ignored; } @@ -237,7 +245,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple return RecommendationsNotificationResult.Ignored; } - this.recommendedExtensions = distinct([...this.recommendedExtensions, ...extensionIds]); + this.recommendedExtensions = distinct([...this.recommendedExtensions, ...extensionIds.filter(isString)]); let extensionsMessage = ''; if (extensions.length === 1) { @@ -422,15 +430,30 @@ export class ExtensionRecommendationNotificationService extends Disposable imple this.visibleNotification = undefined; } - private async getInstallableExtensions(extensionIds: string[]): Promise { + private async getInstallableExtensions(recommendations: Array): Promise { const result: IExtension[] = []; - if (extensionIds.length) { - const extensions = await this.extensionsWorkbenchService.getExtensions(extensionIds.map(id => ({ id })), { source: 'install-recommendations' }, CancellationToken.None); - for (const extension of extensions) { - if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { - result.push(extension); + if (recommendations.length) { + const galleryExtensions: string[] = []; + const resourceExtensions: URI[] = []; + for (const recommendation of recommendations) { + if (typeof recommendation === 'string') { + galleryExtensions.push(recommendation); + } else { + resourceExtensions.push(recommendation); + } + } + if (galleryExtensions.length) { + const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: 'install-recommendations' }, CancellationToken.None); + for (const extension of extensions) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + result.push(extension); + } } } + if (resourceExtensions.length) { + const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true); + result.push(...extensions); + } } return result; } diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index fc28afa9003..bc811fe8ccf 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts @@ -4,13 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { IExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; -export type ExtensionRecommendation = { - readonly extensionId: string; +export type GalleryExtensionRecommendation = { + readonly extension: string; readonly reason: IExtensionRecommendationReason; }; +export type ResourceExtensionRecommendation = { + readonly extension: URI; + readonly reason: IExtensionRecommendationReason; +}; + +export type ExtensionRecommendation = GalleryExtensionRecommendation | ResourceExtensionRecommendation; + export abstract class ExtensionRecommendations extends Disposable { readonly abstract recommendations: ReadonlyArray; diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index e0af925098a..836a263d441 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -8,7 +8,7 @@ import { IExtensionManagementService, IExtensionGalleryService, InstallOperation import { IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { distinct, shuffle } from 'vs/base/common/arrays'; +import { shuffle } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { LifecyclePhase, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -28,6 +28,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { RemoteRecommendations } from 'vs/workbench/contrib/extensions/browser/remoteRecommendations'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; +import { isString } from 'vs/base/common/types'; type IgnoreRecommendationClassification = { owner: 'sandy081'; @@ -151,9 +152,9 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.webRecommendations.recommendations, ]; - for (const { extensionId, reason } of allRecommendations) { - if (this.isExtensionAllowedToBeRecommended(extensionId)) { - output[extensionId.toLowerCase()] = reason; + for (const { extension, reason } of allRecommendations) { + if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension)) { + output[extension.toLowerCase()] = reason; } } @@ -163,8 +164,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte async getConfigBasedRecommendations(): Promise<{ important: string[]; others: string[] }> { await this.configBasedRecommendations.activate(); return { - important: this.toExtensionRecommendations(this.configBasedRecommendations.importantRecommendations), - others: this.toExtensionRecommendations(this.configBasedRecommendations.otherRecommendations) + important: this.toExtensionIds(this.configBasedRecommendations.importantRecommendations), + others: this.toExtensionIds(this.configBasedRecommendations.otherRecommendations) }; } @@ -178,11 +179,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.webRecommendations.recommendations ]; - const extensionIds = distinct(recommendations.map(e => e.extensionId)) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); - + const extensionIds = this.toExtensionIds(recommendations); shuffle(extensionIds, this.sessionSeed); - return extensionIds; } @@ -195,43 +193,50 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.exeBasedRecommendations.importantRecommendations, ]; - const extensionIds = distinct(recommendations.map(e => e.extensionId)) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); - + const extensionIds = this.toExtensionIds(recommendations); shuffle(extensionIds, this.sessionSeed); - return extensionIds; } getKeymapRecommendations(): string[] { - return this.toExtensionRecommendations(this.keymapRecommendations.recommendations); + return this.toExtensionIds(this.keymapRecommendations.recommendations); } getLanguageRecommendations(): string[] { - return this.toExtensionRecommendations(this.languageRecommendations.recommendations); + return this.toExtensionIds(this.languageRecommendations.recommendations); } getRemoteRecommendations(): string[] { - return this.toExtensionRecommendations(this.remoteRecommendations.recommendations); + return this.toExtensionIds(this.remoteRecommendations.recommendations); } - async getWorkspaceRecommendations(): Promise { + async getWorkspaceRecommendations(): Promise> { if (!this.isEnabled()) { return []; } await this.workspaceRecommendations.activate(); - return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations); + const result: Array = []; + for (const { extension } of this.workspaceRecommendations.recommendations) { + if (isString(extension)) { + if (!result.includes(extension.toLowerCase()) && this.isExtensionAllowedToBeRecommended(extension)) { + result.push(extension.toLowerCase()); + } + } else { + result.push(extension); + } + } + return result; } async getExeBasedRecommendations(exe?: string): Promise<{ important: string[]; others: string[] }> { await this.exeBasedRecommendations.activate(); const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe) : { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations }; - return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) }; + return { important: this.toExtensionIds(important), others: this.toExtensionIds(others) }; } getFileBasedRecommendations(): string[] { - return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations); + return this.toExtensionIds(this.fileBasedRecommendations.recommendations); } private onDidInstallExtensions(results: readonly InstallExtensionResult[]): void { @@ -255,10 +260,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte } } - private toExtensionRecommendations(recommendations: ReadonlyArray): string[] { - const extensionIds = distinct(recommendations.map(e => e.extensionId)) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); - + private toExtensionIds(recommendations: ReadonlyArray): string[] { + const extensionIds: string[] = []; + for (const { extension } of recommendations) { + if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension) && !extensionIds.includes(extension.toLowerCase())) { + extensionIds.push(extension.toLowerCase()); + } + } return extensionIds; } @@ -273,8 +281,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.configBasedRecommendations.importantRecommendations.filter( recommendation => !recommendation.whenNotInstalled || recommendation.whenNotInstalled.every(id => installed.every(local => !areSameExtensions(local.identifier, { id })))) ] - .map(({ extensionId }) => extensionId) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); + .map(({ extension }) => extension) + .filter(extension => !isString(extension) || this.isExtensionAllowedToBeRecommended(extension)); if (allowedRecommendations.length) { await this._registerP(timeout(5000)); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index c958741aca7..3ef7b2feb69 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,17 +8,17 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP } from 'vs/workbench/contrib/extensions/common/extensions'; import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -79,6 +79,8 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStringDictionary } from 'vs/base/common/collections'; import { CONTEXT_KEYBINDINGS_EDITOR } from 'vs/workbench/contrib/preferences/common/preferences'; import { DeprecatedExtensionsChecker } from 'vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProductService } from 'vs/platform/product/common/productService'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -121,14 +123,25 @@ Registry.as(ViewContainerExtensions.ViewContainersRegis alwaysUseContainerInfo: true, }, ViewContainerLocation.Sidebar); +class ExtensionsSettingsContributions implements IWorkbenchContribution { -Registry.as(ConfigurationExtensions.Configuration) - .registerConfiguration({ - id: 'extensions', - order: 30, - title: localize('extensionsConfigurationTitle', "Extensions"), - type: 'object', - properties: { + static readonly ID = 'workbench.extensions.settingsContributions'; + + constructor( + @IProductService private readonly productService: IProductService + ) { + Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration({ + id: 'extensions', + order: 30, + title: localize('extensionsConfigurationTitle', "Extensions"), + type: 'object', + properties: this.getExtensionsSettingsProperties() + }); + } + + private getExtensionsSettingsProperties(): IStringDictionary { + const settings: IStringDictionary = { 'extensions.autoUpdate': { enum: [true, 'onlyEnabledExtensions', 'onlySelectedExtensions', false,], enumItemLabels: [ @@ -260,9 +273,27 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', description: localize('extensionsDeferredStartupFinishedActivation', "When enabled, extensions which declare the `onStartupFinished` activation event will be activated after a timeout."), default: false - } + }, + 'extensions.experimental.issueQuickAccess': { + type: 'boolean', + description: localize('extensionsInQuickAccess', "When enabled, extensions can be searched for via Quick Access and report issues from there."), + default: true + }, + }; + + if (this.productService.quality !== 'stable') { + settings['extensions.experimental.supportWorkspaceExtensions'] = { + type: 'boolean', + description: localize('extensions.experimental.supportWorkspaceExtensions', "Enables support for workspace specific local extensions."), + default: false, + scope: ConfigurationScope.APPLICATION + }; } - }); + + return settings; + } + +} const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); jsonRegistry.registerSchema(ExtensionsConfigurationSchemaId, ExtensionsConfigurationSchema); @@ -323,6 +354,15 @@ CommandsRegistry.registerCommand({ 'description': localize('workbench.extensions.installExtension.option.donotSync', "When enabled, VS Code do not sync this extension when Settings Sync is on."), default: false }, + 'justification': { + 'type': ['string', 'object'], + 'description': localize('workbench.extensions.installExtension.option.justification', "Justification for installing the extension. This is a string or an object that can be used to pass any information to the installation handlers. i.e. `{reason: 'This extension wants to open a URI', action: 'Open URI'}` will show a message box with the reason and action upon install."), + }, + 'enable': { + 'type': 'boolean', + 'description': localize('workbench.extensions.installExtension.option.enable', "When enabled, the extension will be enabled if it is installed but disabled. If the extension is already enabled, this has no effect."), + default: false + }, 'context': { 'type': 'object', 'description': localize('workbench.extensions.installExtension.option.context', "Context for the installation. This is a JSON object that can be used to pass any information to the installation handlers. i.e. `{skipWalkthrough: true}` will skip opening the walkthrough upon install."), @@ -332,31 +372,44 @@ CommandsRegistry.registerCommand({ } ] }, - handler: async (accessor, arg: string | UriComponents, options?: { installOnlyNewlyAddedFromExtensionPackVSIX?: boolean; installPreReleaseVersion?: boolean; donotSync?: boolean; context?: IStringDictionary }) => { + handler: async ( + accessor, + arg: string | UriComponents, + options?: { + installOnlyNewlyAddedFromExtensionPackVSIX?: boolean; + installPreReleaseVersion?: boolean; + donotSync?: boolean; + justification?: string | { reason: string; action: string }; + enable?: boolean; + context?: IStringDictionary; + }) => { const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); const extensionManagementService = accessor.get(IWorkbenchExtensionManagementService); + const extensionGalleryService = accessor.get(IExtensionGalleryService); try { if (typeof arg === 'string') { const [id, version] = getIdAndVersion(arg); - const [extension] = await extensionsWorkbenchService.getExtensions([{ id, preRelease: options?.installPreReleaseVersion }], CancellationToken.None); - if (extension) { - const installOptions: InstallOptions = { + const extension = extensionsWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id, uuid: version })); + if (extension?.enablementState === EnablementState.DisabledByExtensionKind) { + const [gallery] = await extensionGalleryService.getExtensions([{ id, preRelease: options?.installPreReleaseVersion }], CancellationToken.None); + if (gallery) { + throw new Error(localize('notFound', "Extension '{0}' not found.", arg)); + } + await extensionManagementService.installFromGallery(gallery, { isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ installPreReleaseVersion: options?.installPreReleaseVersion, installGivenVersion: !!version, context: options?.context - }; - if (extension.gallery && extension.enablementState === EnablementState.DisabledByExtensionKind) { - await extensionManagementService.installFromGallery(extension.gallery, installOptions); - return; - } - if (version) { - await extensionsWorkbenchService.installVersion(extension, version, installOptions); - } else { - await extensionsWorkbenchService.install(extension, installOptions); - } + }); } else { - throw new Error(localize('notFound', "Extension '{0}' not found.", arg)); + await extensionsWorkbenchService.install(arg, { + version, + installPreReleaseVersion: options?.installPreReleaseVersion, + context: options?.context, + justification: options?.justification, + enable: options?.enable, + isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ + }, ProgressLocation.Notification); } } else { const vsix = URI.revive(arg); @@ -1512,7 +1565,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate(), ContextKeyExpr.has('isBuiltinExtension').negate()), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate(), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), order: 3 }, run: async (accessor: ServicesAccessor, id: string) => { @@ -1529,7 +1582,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT), + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), order: 4 }, run: async (accessor: ServicesAccessor, id: string) => { @@ -1570,7 +1623,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate(), ContextKeyExpr.notEquals('extensionSource', 'resource')), order: 2 }, run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkspaceExtensionsConfigService).toggleRecommendation(id) @@ -1716,6 +1769,8 @@ class ExtensionStorageCleaner implements IWorkbenchContribution { } } +registerWorkbenchContribution2(ExtensionsSettingsContributions.ID, ExtensionsSettingsContributions, WorkbenchPhase.BlockStartup); + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Eventually); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 6e0155f3249..aa1601b8649 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { disposeIfDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -73,6 +73,7 @@ import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConst import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IUpdateService } from 'vs/platform/update/common/update'; export class PromptExtensionInstallFailureAction extends Action { @@ -321,6 +322,7 @@ export class InstallAction extends ExtensionAction { @IDialogService private readonly dialogService: IDialogService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { super('extensions.install', localize('install', "Install"), InstallAction.Class, false); this.options = { ...options, isMachineScoped: false }; @@ -501,6 +503,9 @@ export class InstallAction extends ExtensionAction { } getLabel(primary?: boolean): string { + if (this.extension?.isWorkspaceScoped && this.extension.resourceExtension && this.contextService.isInsideWorkspace(this.extension.resourceExtension.location)) { + return localize('install workspace version', "Install Workspace Extension"); + } /* install pre-release version */ if (this.options.installPreReleaseVersion && this.extension?.hasPreReleaseVersion) { return primary ? localize('install pre-release', "Install Pre-Release") : localize('install pre-release version', "Install Pre-Release Version"); @@ -1079,6 +1084,10 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isBuiltinExtension', extension.isBuiltin]); cksOverlay.push(['isDefaultApplicationScopedExtension', extension.local && isApplicationScopedExtension(extension.local.manifest)]); cksOverlay.push(['isApplicationScopedExtension', extension.local && extension.local.isApplicationScoped]); + cksOverlay.push(['isWorkspaceScopedExtension', extension.isWorkspaceScoped]); + if (extension.local) { + cksOverlay.push(['extensionSource', extension.local.source]); + } cksOverlay.push(['extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration]); cksOverlay.push(['extensionHasKeybindings', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.keybindings]); cksOverlay.push(['extensionHasCommands', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes?.commands]); @@ -1386,7 +1395,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { const [extension] = pick.id !== this.extension?.version ? await this.extensionsWorkbenchService.getExtensions([{ id: this.extension!.identifier.id, preRelease: pick.isPreReleaseVersion }], CancellationToken.None) : [this.extension]; await this.extensionsWorkbenchService.install(extension ?? this.extension!, { installPreReleaseVersion: pick.isPreReleaseVersion }); } else { - await this.extensionsWorkbenchService.installVersion(this.extension!, pick.id, { installPreReleaseVersion: pick.isPreReleaseVersion }); + await this.extensionsWorkbenchService.install(this.extension!, { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }); } } catch (error) { this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, pick.latest ? this.extension!.latestVersion : pick.id, InstallOperation.Install, error).run(); @@ -1413,7 +1422,7 @@ export class EnableForWorkspaceAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped) { this.enabled = this.extension.state === ExtensionState.Installed && !this.extensionEnablementService.isEnabled(this.extension.local) && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); @@ -1444,7 +1453,7 @@ export class EnableGloballyAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped) { this.enabled = this.extension.state === ExtensionState.Installed && this.extensionEnablementService.isDisabledGlobally(this.extension.local) && this.extensionEnablementService.canChangeEnablement(this.extension.local); @@ -1478,7 +1487,7 @@ export class DisableForWorkspaceAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); @@ -1511,7 +1520,7 @@ export class DisableGloballyAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) && this.extensionEnablementService.canChangeEnablement(this.extension.local); @@ -1553,18 +1562,21 @@ export class DisableDropDownAction extends ActionWithDropDownAction { } -export class ReloadAction extends ExtensionAction { +export class ExtensionRuntimeStateAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; - private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${ExtensionRuntimeStateAction.EnabledClass} disabled`; updateWhenCounterExtensionChanges: boolean = true; constructor( @IHostService private readonly hostService: IHostService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IUpdateService private readonly updateService: IUpdateService, @IExtensionService private readonly extensionService: IExtensionService, + @IProductService private readonly productService: IProductService, ) { - super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); + super('extensions.runtimeState', '', ExtensionRuntimeStateAction.DisabledClass, false); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); this.update(); } @@ -1572,27 +1584,58 @@ export class ReloadAction extends ExtensionAction { update(): void { this.enabled = false; this.tooltip = ''; + this.class = ExtensionRuntimeStateAction.DisabledClass; + if (!this.extension) { return; } + const state = this.extension.state; if (state === ExtensionState.Installing || state === ExtensionState.Uninstalling) { return; } + if (this.extension.local && this.extension.local.manifest && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.localizations && this.extension.local.manifest.contributes.localizations.length > 0) { return; } - const reloadTooltip = this.extension.reloadRequiredStatus; - this.enabled = reloadTooltip !== undefined; - this.label = reloadTooltip !== undefined ? localize('reload required', 'Reload Required') : ''; - this.tooltip = reloadTooltip !== undefined ? reloadTooltip : ''; + const runtimeState = this.extension.runtimeState; + if (!runtimeState) { + return; + } - this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; + this.enabled = true; + this.class = ExtensionRuntimeStateAction.EnabledClass; + this.tooltip = runtimeState.reason; + this.label = runtimeState.action === ExtensionRuntimeActionType.ReloadWindow ? localize('reload window', 'Reload Window') + : runtimeState.action === ExtensionRuntimeActionType.RestartExtensions ? localize('restart extensions', 'Restart Extensions') + : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart to Update') + : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; } - override run(): Promise { - return Promise.resolve(this.hostService.reload()); + override async run(): Promise { + const runtimeState = this.extension?.runtimeState; + + if (runtimeState?.action === ExtensionRuntimeActionType.ReloadWindow) { + return this.hostService.reload(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions) { + return this.extensionsWorkbenchService.updateRunningExtensions(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.DownloadUpdate) { + return this.updateService.downloadUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.ApplyUpdate) { + return this.updateService.applyUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.QuitAndInstall) { + return this.updateService.quitAndInstall(); + } + } } @@ -2501,7 +2544,7 @@ export class ExtensionStatusAction extends ExtensionAction { const isEnabled = this.workbenchExtensionEnablementService.isEnabled(this.extension.local); const isRunning = this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); - if (isEnabled && isRunning) { + if (!this.extension.isWorkspaceScoped && isEnabled && isRunning) { if (this.extension.enablementState === EnablementState.EnabledWorkspace) { this.updateStatus({ message: new MarkdownString(localize('workspace enabled', "This extension is enabled for this workspace by the user.")) }, true); return; diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index d5058c51b43..2466a15d0e9 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -13,7 +13,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; @@ -117,7 +117,7 @@ export class Renderer implements IPagedRenderer { const actions = [ this.instantiationService.createInstance(ExtensionStatusLabelAction), this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, true), - this.instantiationService.createInstance(ReloadAction), + this.instantiationService.createInstance(ExtensionRuntimeStateAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', [[this.instantiationService.createInstance(UpdateAction, false)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), this.instantiationService.createInstance(InstallDropdownAction), @@ -223,7 +223,7 @@ export class Renderer implements IPagedRenderer { data.description.textContent = extension.description; const updatePublisher = () => { - data.publisherDisplayName.textContent = extension.publisherDisplayName; + data.publisherDisplayName.textContent = !extension.resourceExtension && extension.local?.source !== 'resource' ? extension.publisherDisplayName : ''; }; updatePublisher(); Event.filter(this.extensionsWorkbenchService.onChange, e => !!e && areSameExtensions(e.identifier, extension.identifier))(() => updatePublisher(), this, data.extensionDisposables); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 38c0e666af1..afa823bb849 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -86,7 +86,7 @@ const SortByUpdateDateContext = new RawContextKey('sortByUpdateDate', f const REMOTE_CATEGORY: ILocalizedString = localize2({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"); -export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { +export class ExtensionsViewletViewsContribution extends Disposable implements IWorkbenchContribution { private readonly container: ViewContainer; @@ -96,6 +96,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService private readonly contextKeyService: IContextKeyService ) { + super(); + this.container = viewDescriptorService.getViewContainerById(VIEWLET_ID)!; this.registerViews(); } @@ -172,7 +174,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio }); if (server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer) { - registerAction2(class InstallLocalExtensionsInRemoteAction2 extends Action2 { + this._register(registerAction2(class InstallLocalExtensionsInRemoteAction2 extends Action2 { constructor() { super({ id: 'workbench.extensions.installLocalExtensions', @@ -192,12 +194,12 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio run(accessor: ServicesAccessor): Promise { return accessor.get(IInstantiationService).createInstance(InstallLocalExtensionsInRemoteAction).run(); } - }); + })); } } if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { + this._register(registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { constructor() { super({ id: 'workbench.extensions.actions.installLocalExtensionsInRemote', @@ -209,7 +211,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio run(accessor: ServicesAccessor): Promise { return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run(); } - }); + })); } /* @@ -853,19 +855,19 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution private onServiceChange(): void { this.badgeHandle.clear(); - const extensionsReloadRequired = this.extensionsWorkbenchService.installed.filter(e => e.reloadRequiredStatus !== undefined); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !extensionsReloadRequired.includes(e) ? 1 : 0), 0); - const newBadgeNumber = outdated + extensionsReloadRequired.length; + const actionRequired = this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); + const newBadgeNumber = outdated + actionRequired.length; if (newBadgeNumber > 0) { let msg = ''; if (outdated) { msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); } - if (outdated > 0 && extensionsReloadRequired.length > 0) { + if (outdated > 0 && actionRequired.length > 0) { msg += ', '; } - if (extensionsReloadRequired.length) { - msg += extensionsReloadRequired.length === 1 ? localize('extensionToReload', '{0} requires reload', extensionsReloadRequired.length) : localize('extensionsToReload', '{0} require reload', extensionsReloadRequired.length); + if (actionRequired.length) { + msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); } const badge = new NumberBadge(newBadgeNumber, () => msg); this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge }); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index adcd04b081f..09f88bf6bf8 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { isCancellationError, getErrorMessage } from 'vs/base/common/errors'; import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions, getExtensionDependencies } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -42,7 +42,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/browser/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -57,6 +57,9 @@ import { isOfflineError } from 'vs/base/parts/request/common/request'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IExtensionFeatureRenderer, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { URI } from 'vs/base/common/uri'; +import { isString } from 'vs/base/common/types'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; export const NONE_CATEGORY = 'none'; @@ -148,6 +151,7 @@ export class ExtensionsListView extends ViewPane { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, + @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService ) { super({ @@ -183,7 +187,20 @@ export class ExtensionsListView extends ViewPane { const messageBox = append(messageContainer, $('.message')); const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { hoverOptions: { position: () => { return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; } } }); + const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { + hoverOptions: { + position: () => { + const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); + if (viewLocation === ViewContainerLocation.Sidebar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } + if (viewLocation === ViewContainerLocation.AuxiliaryBar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + return HoverPosition.RIGHT; + } + } + }); this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { multipleSelectionSupport: false, setRowLineHeight: false, @@ -509,7 +526,7 @@ export class ExtensionsListView extends ViewPane { result = local.filter(e => !e.isBuiltin && matchingText(e)); result = this.sortExtensions(result, options); } else { - result = local.filter(e => (!e.isBuiltin || e.outdated || e.reloadRequiredStatus !== undefined) && matchingText(e)); + result = local.filter(e => (!e.isBuiltin || e.outdated || e.runtimeState !== undefined) && matchingText(e)); const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(e.identifier.value, e); return result; }, new ExtensionIdentifierMap()); const defaultSort = (e1: IExtension, e2: IExtension) => { @@ -538,21 +555,21 @@ export class ExtensionsListView extends ViewPane { }; const outdated: IExtension[] = []; - const reloadRequired: IExtension[] = []; + const actionRequired: IExtension[] = []; const noActionRequired: IExtension[] = []; result.forEach(e => { if (e.outdated) { outdated.push(e); } - else if (e.reloadRequiredStatus) { - reloadRequired.push(e); + else if (e.runtimeState) { + actionRequired.push(e); } else { noActionRequired.push(e); } }); - result = [...outdated.sort(defaultSort), ...reloadRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; + result = [...outdated.sort(defaultSort), ...actionRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; } return result; } @@ -865,20 +882,35 @@ export class ExtensionsListView extends ViewPane { return new PagedModel([]); } - protected async getInstallableRecommendations(recommendations: string[], options: IQueryOptions, token: CancellationToken): Promise { + protected async getInstallableRecommendations(recommendations: Array, options: IQueryOptions, token: CancellationToken): Promise { const result: IExtension[] = []; if (recommendations.length) { - const extensions = await this.extensionsWorkbenchService.getExtensions(recommendations.map(id => ({ id })), { source: options.source }, token); - for (const extension of extensions) { - if (extension.gallery && !extension.deprecationInfo && (await this.extensionManagementService.canInstall(extension.gallery))) { - result.push(extension); + const galleryExtensions: string[] = []; + const resourceExtensions: URI[] = []; + for (const recommendation of recommendations) { + if (typeof recommendation === 'string') { + galleryExtensions.push(recommendation); + } else { + resourceExtensions.push(recommendation); + } + } + if (galleryExtensions.length) { + const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: options.source }, token); + for (const extension of extensions) { + if (extension.gallery && !extension.deprecationInfo && (await this.extensionManagementService.canInstall(extension.gallery))) { + result.push(extension); + } } } + if (resourceExtensions.length) { + const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true); + result.push(...extensions); + } } return result; } - protected async getWorkspaceRecommendations(): Promise { + protected async getWorkspaceRecommendations(): Promise> { const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations(); const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations(); for (const configBasedRecommendation of important) { @@ -892,8 +924,7 @@ export class ExtensionsListView extends ViewPane { private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const recommendations = await this.getWorkspaceRecommendations(); const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)); - const result: IExtension[] = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(result); + return new PagedModel(installableRecommendations); } private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { @@ -938,7 +969,7 @@ export class ExtensionsListView extends ViewPane { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)) .map(e => e.identifier.id.toLowerCase()); const workspaceRecommendations = (await this.getWorkspaceRecommendations()) - .map(extensionId => extensionId.toLowerCase()); + .map(extensionId => isString(extensionId) ? extensionId.toLowerCase() : extensionId); return distinct( flatten(await Promise.all([ @@ -961,12 +992,10 @@ export class ExtensionsListView extends ViewPane { this.extensionRecommendationsService.getImportantRecommendations(), this.extensionRecommendationsService.getFileBasedRecommendations(), this.extensionRecommendationsService.getOtherRecommendations() - ])).filter(extensionId => !local.includes(extensionId.toLowerCase()) - ), extensionId => extensionId.toLowerCase()); + ])).filter(extensionId => !isString(extensionId) || !local.includes(extensionId.toLowerCase()))); const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token); - const result: IExtension[] = coalesce(allRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(result.slice(0, 8)); + return new PagedModel(installableRecommendations.slice(0, 8)); } private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { @@ -974,8 +1003,7 @@ export class ExtensionsListView extends ViewPane { const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]); const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token)) .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); - const result = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(this.sortExtensions(result, options)); + return new PagedModel(this.sortExtensions(installableRecommendations, options)); } private setModel(model: IPagedModel, error?: any, donotResetScrollTop?: boolean) { @@ -1313,12 +1341,14 @@ export class StaticQueryExtensionsView extends ExtensionsListView { @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, + @IUriIdentityService uriIdentityService: IUriIdentityService, @ILogService logService: ILogService ) { super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, extensionRecommendationsService, telemetryService, configurationService, contextService, extensionManagementServerService, extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService, - preferencesService, storageService, workspaceTrustManagementService, extensionEnablementService, layoutService, extensionFeaturesManagementService, logService); + preferencesService, storageService, workspaceTrustManagementService, extensionEnablementService, layoutService, extensionFeaturesManagementService, + uriIdentityService, logService); } override show(): Promise> { @@ -1451,18 +1481,30 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView imple return model; } - private async getInstallableWorkspaceRecommendations() { + private async getInstallableWorkspaceRecommendations(): Promise { const installed = (await this.extensionsWorkbenchService.queryLocal()) .filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind const recommendations = (await this.getWorkspaceRecommendations()) - .filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + .filter(recommendation => installed.every(local => isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.local?.location))); return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None); } async installWorkspaceRecommendations(): Promise { const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); if (installableRecommendations.length) { - await this.extensionManagementService.installGalleryExtensions(installableRecommendations.map(i => ({ extension: i.gallery!, options: {} }))); + const galleryExtensions: InstallExtensionInfo[] = []; + const resourceExtensions: IExtension[] = []; + for (const recommendation of installableRecommendations) { + if (recommendation.gallery) { + galleryExtensions.push({ extension: recommendation.gallery, options: {} }); + } else { + resourceExtensions.push(recommendation); + } + } + await Promise.all([ + this.extensionManagementService.installGalleryExtensions(galleryExtensions), + ...resourceExtensions.map(extension => this.extensionsWorkbenchService.install(extension)) + ]); } else { this.notificationService.notify({ severity: Severity.Info, diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 4ad3680b7d7..50c5e355972 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -31,7 +31,7 @@ import { URI } from 'vs/base/common/uri'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import Severity from 'vs/base/common/severity'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { Color } from 'vs/base/common/color'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -41,7 +41,8 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -209,6 +210,14 @@ export class VerifiedPublisherWidget extends ExtensionWidget { return; } + if (this.extension.resourceExtension) { + return; + } + + if (this.extension.local?.source === 'resource') { + return; + } + const publisherDomainLink = URI.parse(this.extension.publisherDomain.link); const verifiedPublisher = append(this.container, $('span.extension-verified-publisher.clickable')); append(verifiedPublisher, renderIcon(verifiedPublisherIcon)); @@ -529,6 +538,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService, @IThemeService private readonly themeService: IThemeService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { super(); } @@ -595,6 +605,16 @@ export class ExtensionHoverWidget extends ExtensionWidget { } } + const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined); + if (location) { + if (this.extension.isWorkspaceScoped && this.contextService.isInsideWorkspace(location)) { + markdown.appendMarkdown(localize('workspace extension', "Workspace Extension")); + } else { + markdown.appendMarkdown(localize('local extension', "Local Extension")); + } + markdown.appendText(`\n`); + } + if (this.extension.description) { markdown.appendMarkdown(`${this.extension.description}`); markdown.appendText(`\n`); @@ -616,10 +636,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension); const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); const extensionStatus = this.extensionStatusAction.status; - const reloadRequiredMessage = this.extension.reloadRequiredStatus; + const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); - if (extensionRuntimeStatus || extensionStatus || reloadRequiredMessage || recommendationMessage || preReleaseMessage) { + if (extensionRuntimeStatus || extensionStatus || runtimeState || recommendationMessage || preReleaseMessage) { markdown.appendMarkdown(`---`); markdown.appendText(`\n`); @@ -656,9 +676,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendText(`\n`); } - if (reloadRequiredMessage) { + if (runtimeState) { markdown.appendMarkdown(`$(${infoIcon.id}) `); - markdown.appendMarkdown(`${reloadRequiredMessage}`); + markdown.appendMarkdown(`${runtimeState.reason}`); markdown.appendText(`\n`); } diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index fa11076ab65..d2e12644968 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; import { Event, Emitter } from 'vs/base/common/event'; -import { index } from 'vs/base/common/arrays'; +import { firstOrDefault, index } from 'vs/base/common/arrays'; import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise } from 'vs/base/common/async'; import { CancellationError, isCancellationError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -14,16 +14,17 @@ import { IPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, - InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, - IExtensionsControlManifest, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX + InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, WEB_EXTENSION_TAG, InstallExtensionResult, + IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, + InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -39,7 +40,7 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { IProductService } from 'vs/platform/product/common/productService'; import { FileAccess } from 'vs/base/common/network'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; -import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataAutoSyncService, IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { isBoolean, isString, isUndefined } from 'vs/base/common/types'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; @@ -51,6 +52,10 @@ import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUti import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { mainWindow } from 'vs/base/browser/window'; +import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; +import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; interface IExtensionStateProvider { (extension: Extension): T; @@ -70,19 +75,23 @@ type ExtensionsLoadClassification = { export class Extension implements IExtension { public enablementState: EnablementState = EnablementState.EnabledGlobally; + public readonly resourceExtension: IResourceExtension | undefined; constructor( private stateProvider: IExtensionStateProvider, - private runtimeStateProvider: IExtensionStateProvider, + private runtimeStateProvider: IExtensionStateProvider, public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, + private readonly resourceExtensionInfo: { resourceExtension: IResourceExtension; isWorkspaceScoped: boolean } | undefined, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService - ) { } + ) { + this.resourceExtension = resourceExtensionInfo?.resourceExtension; + } get type(): ExtensionType { return this.local ? this.local.type : ExtensionType.User; @@ -92,8 +101,21 @@ export class Extension implements IExtension { return this.local ? this.local.isBuiltin : false; } + get isWorkspaceScoped(): boolean { + if (this.local) { + return this.local.isWorkspaceScoped; + } + if (this.resourceExtensionInfo) { + return this.resourceExtensionInfo.isWorkspaceScoped; + } + return false; + } + get name(): string { - return this.gallery ? this.gallery.name : this.local!.manifest.name; + if (this.gallery) { + return this.gallery.name; + } + return this.getManifestFromLocalOrResource()?.name ?? ''; } get displayName(): string { @@ -101,22 +123,28 @@ export class Extension implements IExtension { return this.gallery.displayName || this.gallery.name; } - return this.local!.manifest.displayName || this.local!.manifest.name; + return this.getManifestFromLocalOrResource()?.displayName ?? this.name; } get identifier(): IExtensionIdentifier { if (this.gallery) { return this.gallery.identifier; } + if (this.resourceExtension) { + return this.resourceExtension.identifier; + } return this.local!.identifier; } get uuid(): string | undefined { - return this.gallery ? this.gallery.identifier.uuid : this.local!.identifier.uuid; + return this.gallery ? this.gallery.identifier.uuid : this.local?.identifier.uuid; } get publisher(): string { - return this.gallery ? this.gallery.publisher : this.local!.manifest.publisher; + if (this.gallery) { + return this.gallery.publisher; + } + return this.getManifestFromLocalOrResource()?.publisher ?? ''; } get publisherDisplayName(): string { @@ -128,7 +156,7 @@ export class Extension implements IExtension { return this.local.publisherDisplayName; } - return this.local!.manifest.publisher; + return this.publisher; } get publisherUrl(): URI | undefined { @@ -156,11 +184,11 @@ export class Extension implements IExtension { } get latestVersion(): string { - return this.gallery ? this.gallery.version : this.local!.manifest.version; + return this.gallery ? this.gallery.version : this.getManifestFromLocalOrResource()?.version ?? ''; } get description(): string { - return this.gallery ? this.gallery.description : this.local!.manifest.description || ''; + return this.gallery ? this.gallery.description : this.getManifestFromLocalOrResource()?.description ?? ''; } get url(): string | undefined { @@ -172,11 +200,11 @@ export class Extension implements IExtension { } get iconUrl(): string { - return this.galleryIconUrl || this.localIconUrl || this.defaultIconUrl; + return this.galleryIconUrl || this.resourceExtensionIconUrl || this.localIconUrl || this.defaultIconUrl; } get iconUrlFallback(): string { - return this.galleryIconUrlFallback || this.localIconUrl || this.defaultIconUrl; + return this.galleryIconUrlFallback || this.resourceExtensionIconUrl || this.localIconUrl || this.defaultIconUrl; } private get localIconUrl(): string | null { @@ -186,6 +214,13 @@ export class Extension implements IExtension { return null; } + private get resourceExtensionIconUrl(): string | null { + if (this.resourceExtension?.manifest.icon) { + return FileAccess.uriToBrowserUri(resources.joinPath(this.resourceExtension.location, this.resourceExtension.manifest.icon)).toString(true); + } + return null; + } + private get galleryIconUrl(): string | null { return this.gallery?.assets.icon ? this.gallery.assets.icon.uri : null; } @@ -271,7 +306,7 @@ export class Extension implements IExtension { && semver.eq(this.latestVersion, this.version); } - get reloadRequiredStatus(): string | undefined { + get runtimeState(): ExtensionRuntimeState | undefined { return this.runtimeStateProvider(this); } @@ -280,8 +315,10 @@ export class Extension implements IExtension { if (gallery) { return getGalleryExtensionTelemetryData(gallery); + } else if (local) { + return getLocalExtensionTelemetryData(local); } else { - return getLocalExtensionTelemetryData(local!); + return {}; } } @@ -305,7 +342,7 @@ export class Extension implements IExtension { } get hasReleaseVersion(): boolean { - return !!this.gallery?.hasReleaseVersion; + return !!this.resourceExtension || !!this.gallery?.hasReleaseVersion; } private getLocal(): ILocalExtension | undefined { @@ -326,6 +363,10 @@ export class Extension implements IExtension { return null; } + if (this.resourceExtension) { + return this.resourceExtension.manifest; + } + return null; } @@ -338,6 +379,10 @@ export class Extension implements IExtension { return true; } + if (this.resourceExtension?.readmeUri) { + return true; + } + return this.type === ExtensionType.System; } @@ -363,6 +408,11 @@ ${this.description} `); } + if (this.resourceExtension?.readmeUri) { + const content = await this.fileService.readFile(this.resourceExtension?.readmeUri); + return content.value.toString(); + } + return Promise.reject(new Error('not available')); } @@ -397,13 +447,16 @@ ${this.description} } get categories(): readonly string[] { - const { local, gallery } = this; + const { local, gallery, resourceExtension } = this; if (local && local.manifest.categories && !this.outdated) { return local.manifest.categories; } if (gallery) { return gallery.categories; } + if (resourceExtension) { + return resourceExtension.manifest.categories ?? []; + } return []; } @@ -416,26 +469,42 @@ ${this.description} } get dependencies(): string[] { - const { local, gallery } = this; + const { local, gallery, resourceExtension } = this; if (local && local.manifest.extensionDependencies && !this.outdated) { return local.manifest.extensionDependencies; } if (gallery) { return gallery.properties.dependencies || []; } + if (resourceExtension) { + return resourceExtension.manifest.extensionDependencies || []; + } return []; } get extensionPack(): string[] { - const { local, gallery } = this; + const { local, gallery, resourceExtension } = this; if (local && local.manifest.extensionPack && !this.outdated) { return local.manifest.extensionPack; } if (gallery) { return gallery.properties.extensionPack || []; } + if (resourceExtension) { + return resourceExtension.manifest.extensionPack || []; + } return []; } + + private getManifestFromLocalOrResource(): IExtensionManifest | null { + if (this.local) { + return this.local.manifest; + } + if (this.resourceExtension) { + return this.resourceExtension.manifest; + } + return null; + } } const EXTENSIONS_AUTO_UPDATE_KEY = 'extensions.autoUpdate'; @@ -460,9 +529,11 @@ class Extensions extends Disposable { constructor( readonly server: IExtensionManagementServer, private readonly stateProvider: IExtensionStateProvider, - private readonly runtimeStateProvider: IExtensionStateProvider, + private readonly runtimeStateProvider: IExtensionStateProvider, + private readonly isWorkspaceServer: boolean, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -475,6 +546,29 @@ class Extensions extends Disposable { this._register(server.extensionManagementService.onDidChangeProfile(() => this.reset())); this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e))); this._register(Event.any(this.onChange, this.onReset)(() => this._local = undefined)); + if (this.isWorkspaceServer) { + this._register(this.workbenchExtensionManagementService.onInstallExtension(e => { + if (e.workspaceScoped) { + this.onInstallExtension(e); + } + })); + this._register(this.workbenchExtensionManagementService.onDidInstallExtensions(e => { + const result = e.filter(e => e.workspaceScoped); + if (result.length) { + this.onDidInstallExtensions(result); + } + })); + this._register(this.workbenchExtensionManagementService.onUninstallExtension(e => { + if (e.workspaceScoped) { + this.onUninstallExtension(e.identifier); + } + })); + this._register(this.workbenchExtensionManagementService.onDidUninstallExtension(e => { + if (e.workspaceScoped) { + this.onDidUninstallExtension(e); + } + })); + } } private _local: IExtension[] | undefined; @@ -493,15 +587,14 @@ class Extensions extends Disposable { return this._local; } - async queryInstalled(): Promise { - await this.fetchInstalledExtensions(); + async queryInstalled(productVersion: IProductVersion): Promise { + await this.fetchInstalledExtensions(productVersion); this._onChange.fire(undefined); return this.local; } - async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[]): Promise { - let hasChanged: boolean = false; - const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions); + async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise { + const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion); for (const [extension, gallery] of extensions) { // update metadata of the extension if it does not exist if (extension.local && !extension.local.identifier.uuid) { @@ -510,20 +603,18 @@ class Extensions extends Disposable { if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) { extension.gallery = gallery; this._onChange.fire({ extension }); - hasChanged = true; } } - return hasChanged; } - private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[]): Promise<[Extension, IGalleryExtension][]> { + private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise<[Extension, IGalleryExtension][]> { const mappedExtensions = this.mapInstalledExtensionWithGalleryExtension(galleryExtensions); const targetPlatform = await this.server.extensionManagementService.getTargetPlatform(); const compatibleGalleryExtensions: IGalleryExtension[] = []; const compatibleGalleryExtensionsToFetch: IExtensionInfo[] = []; await Promise.allSettled(mappedExtensions.map(async ([extension, gallery]) => { if (extension.local) { - if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform)) { + if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform, productVersion)) { compatibleGalleryExtensions.push(gallery); } else { compatibleGalleryExtensionsToFetch.push({ ...extension.local.identifier, preRelease: extension.local.preRelease }); @@ -531,7 +622,7 @@ class Extensions extends Disposable { } })); if (compatibleGalleryExtensionsToFetch.length) { - const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true }, CancellationToken.None); + const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true, productVersion }, CancellationToken.None); compatibleGalleryExtensions.push(...result); } return this.mapInstalledExtensionWithGalleryExtension(compatibleGalleryExtensions); @@ -552,9 +643,11 @@ class Extensions extends Disposable { continue; } } - const gallery = byID.get(installed.identifier.id.toLowerCase()); - if (gallery) { - mappedExtensions.push([installed, gallery]); + if (installed.local?.source !== 'resource') { + const gallery = byID.get(installed.identifier.id.toLowerCase()); + if (gallery) { + mappedExtensions.push([installed, gallery]); + } } } return mappedExtensions; @@ -581,16 +674,19 @@ class Extensions extends Disposable { private onInstallExtension(event: InstallExtensionEvent): void { const { source } = event; if (source && !URI.isUri(source)) { - const extension = this.installed.filter(e => areSameExtensions(e.identifier, source.identifier))[0] - || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, undefined, source); + const extension = this.installed.find(e => areSameExtensions(e.identifier, source.identifier)) + ?? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, undefined, source, undefined); this.installing.push(extension); this._onChange.fire({ extension }); } } - private async fetchInstalledExtensions(): Promise { + private async fetchInstalledExtensions(productVersion?: IProductVersion): Promise { const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); - const all = await this.server.extensionManagementService.getInstalled(); + const all = await this.server.extensionManagementService.getInstalled(undefined, undefined, productVersion); + if (this.isWorkspaceServer) { + all.push(...await this.workbenchExtensionManagementService.getInstalledWorkspaceExtensions(true)); + } // dedup user and system extensions by giving priority to user extensions. const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => { @@ -602,7 +698,7 @@ class Extensions extends Disposable { const byId = index(this.installed, e => e.local ? e.local.identifier.id : e.identifier.id); this.installed = installed.map(local => { - const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined); + const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined); extension.local = local; extension.enablementState = this.extensionEnablementService.getEnablementState(local); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); @@ -627,7 +723,7 @@ class Extensions extends Disposable { this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; let extension: Extension | undefined = installingExtension ? installingExtension - : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined) + : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined) : undefined; if (extension) { if (local) { @@ -646,7 +742,7 @@ class Extensions extends Disposable { } } this._onChange.fire(!local || !extension ? undefined : { extension, operation: event.operation }); - if (extension && extension.local && !extension.gallery) { + if (extension && extension.local && !extension.gallery && extension.local.source !== 'resource') { await this.syncInstalledExtensionWithGallery(extension); } } @@ -779,6 +875,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IFileService private readonly fileService: IFileService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IStorageService private readonly storageService: IStorageService, + @IDialogService private readonly dialogService: IDialogService, + @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, + @IUpdateService private readonly updateService: IUpdateService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -787,19 +887,34 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { - this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.localExtensions = this._register(instantiationService.createInstance(Extensions, + extensionManagementServerService.localExtensionManagementServer, + ext => this.getExtensionState(ext), + ext => this.getRuntimeState(ext), + !extensionManagementServerService.remoteExtensionManagementServer + )); this._register(this.localExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.localExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.localExtensions); } if (extensionManagementServerService.remoteExtensionManagementServer) { - this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, + extensionManagementServerService.remoteExtensionManagementServer, + ext => this.getExtensionState(ext), + ext => this.getRuntimeState(ext), + true + )); this._register(this.remoteExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.remoteExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.remoteExtensions); } if (extensionManagementServerService.webExtensionManagementServer) { - this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.webExtensions = this._register(instantiationService.createInstance(Extensions, + extensionManagementServerService.webExtensionManagementServer, + ext => this.getExtensionState(ext), + ext => this.getRuntimeState(ext), + !(extensionManagementServerService.remoteExtensionManagementServer || extensionManagementServerService.localExtensionManagementServer) + )); this._register(this.webExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.webExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.webExtensions); @@ -854,6 +969,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); + this._register(this.updateService.onStateChange(e => { + if (!this.isAutoUpdateEnabled()) { + return; + } + if ((e.type === StateType.CheckingForUpdates && e.explicit) || e.type === StateType.AvailableForDownload || e.type === StateType.Downloaded) { + this.eventuallyCheckForUpdates(true); + } + })); // Update AutoUpdate Contexts this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); @@ -953,19 +1076,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension async queryLocal(server?: IExtensionManagementServer): Promise { if (server) { if (this.localExtensions && this.extensionManagementServerService.localExtensionManagementServer === server) { - return this.localExtensions.queryInstalled(); + return this.localExtensions.queryInstalled(this.getProductVersion()); } if (this.remoteExtensions && this.extensionManagementServerService.remoteExtensionManagementServer === server) { - return this.remoteExtensions.queryInstalled(); + return this.remoteExtensions.queryInstalled(this.getProductVersion()); } if (this.webExtensions && this.extensionManagementServerService.webExtensionManagementServer === server) { - return this.webExtensions.queryInstalled(); + return this.webExtensions.queryInstalled(this.getProductVersion()); } } if (this.localExtensions) { try { - await this.localExtensions.queryInstalled(); + await this.localExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -973,7 +1096,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.remoteExtensions) { try { - await this.remoteExtensions.queryInstalled(); + await this.remoteExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -981,7 +1104,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.webExtensions) { try { - await this.webExtensions.queryInstalled(); + await this.webExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -1031,6 +1154,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return galleryExtensions.map(gallery => this.fromGallery(gallery, extensionsControlManifest)); } + async getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise { + const resourceExtensions = await this.extensionManagementService.getExtensions(locations); + return resourceExtensions.map(resourceExtension => this.getInstalledExtensionMatchingLocation(resourceExtension.location) + ?? this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, undefined, { resourceExtension, isWorkspaceScoped })); + } + private resolveQueryText(text: string): string { text = text.replace(/@web/g, `tag:"${WEB_EXTENSION_TAG}"`); @@ -1057,7 +1186,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension { let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); } return extension; @@ -1069,7 +1198,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (installed.identifier.uuid === gallery.identifier.uuid) { return installed; } - } else { + } else if (installed.local?.source !== 'resource') { if (areSameExtensions(installed.identifier, gallery.identifier)) { // Installed from other sources return installed; } @@ -1078,6 +1207,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return null; } + private getInstalledExtensionMatchingLocation(location: URI): IExtension | null { + return this.local.find(e => e.local && this.uriIdentityService.extUri.isEqualOrParent(location, e.local?.location)) ?? null; + } + async open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise { if (typeof extension === 'string') { const id = extension; @@ -1099,15 +1232,59 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } - private getReloadStatus(extension: IExtension): string | undefined { + async updateRunningExtensions(): Promise { + const toAdd: ILocalExtension[] = []; + const toRemove: string[] = []; + + const extensionsToCheck = [...this.local]; + + const notExistingRunningExtensions = this.extensionService.extensions.filter(e => !this.local.some(local => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, local.identifier))); + if (notExistingRunningExtensions.length) { + const extensions = await this.getExtensions(notExistingRunningExtensions.map(e => ({ id: e.identifier.value })), CancellationToken.None); + extensionsToCheck.push(...extensions); + } + + for (const extension of extensionsToCheck) { + const runtimeState = extension.runtimeState; + if (!runtimeState || runtimeState.action !== ExtensionRuntimeActionType.RestartExtensions) { + continue; + } + if (extension.state === ExtensionState.Uninstalled) { + toRemove.push(extension.identifier.id); + continue; + } + if (!extension.local) { + continue; + } + const isEnabled = this.extensionEnablementService.isEnabled(extension.local); + if (isEnabled) { + const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); + if (runningExtension) { + toRemove.push(runningExtension.identifier.value); + } + toAdd.push(extension.local); + } else { + toRemove.push(extension.identifier.id); + } + } + if (toAdd.length || toRemove.length) { + if (await this.extensionService.stopExtensionHosts(nls.localize('restart', "Enable or Disable extensions"))) { + await this.extensionService.startExtensionHosts({ toAdd, toRemove }); + } + } + } + + private getRuntimeState(extension: IExtension): ExtensionRuntimeState | undefined { const isUninstalled = extension.state === ExtensionState.Uninstalled; const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); + const reloadAction = this.extensionManagementServerService.remoteExtensionManagementServer ? ExtensionRuntimeActionType.ReloadWindow : ExtensionRuntimeActionType.RestartExtensions; + const reloadActionLabel = reloadAction === ExtensionRuntimeActionType.ReloadWindow ? nls.localize('reload', "reload window") : nls.localize('restart extensions', "restart extensions"); if (isUninstalled) { const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); if (!canRemoveRunningExtension && isSameExtensionRunning && !runningExtension.isUnderDevelopment) { - return nls.localize('postUninstallTooltip', "Please reload Visual Studio Code to complete the uninstallation of this extension."); + return { action: reloadAction, reason: nls.localize('postUninstallTooltip', "Please {0} to complete the uninstallation of this extension.", reloadActionLabel) }; } return undefined; } @@ -1127,7 +1304,25 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isSameExtensionRunning) { // Different version or target platform of same extension is running. Requires reload to run the current version if (!runningExtension.isUnderDevelopment && (extension.version !== runningExtension.version || extension.local.targetPlatform !== runningExtension.targetPlatform)) { - return nls.localize('postUpdateTooltip', "Please reload Visual Studio Code to enable the updated extension."); + const productCurrentVersion = this.getProductCurrentVersion(); + const productUpdateVersion = this.getProductUpdateVersion(); + if (productUpdateVersion + && !isEngineValid(extension.local.manifest.engines.vscode, productCurrentVersion.version, productCurrentVersion.date) + && isEngineValid(extension.local.manifest.engines.vscode, productUpdateVersion.version, productUpdateVersion.date) + ) { + const state = this.updateService.state; + if (state.type === StateType.AvailableForDownload) { + return { action: ExtensionRuntimeActionType.DownloadUpdate, reason: nls.localize('postUpdateDownloadTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Downloaded) { + return { action: ExtensionRuntimeActionType.ApplyUpdate, reason: nls.localize('postUpdateUpdateTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Ready) { + return { action: ExtensionRuntimeActionType.QuitAndInstall, reason: nls.localize('postUpdateRestartTooltip', "Please restart {0} to enable the updated extension.", this.productService.nameLong) }; + } + return undefined; + } + return { action: reloadAction, reason: nls.localize('postUpdateTooltip', "Please {0} to enable the updated extension.", reloadActionLabel) }; } if (this.extensionsServers.length > 1) { @@ -1135,12 +1330,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensionInOtherServer) { // This extension prefers to run on UI/Local side but is running in remote if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.localExtensionManagementServer) { - return nls.localize('enable locally', "Please reload Visual Studio Code to enable this extension locally."); + return { action: reloadAction, reason: nls.localize('enable locally', "Please {0} to enable this extension locally.", reloadActionLabel) }; } // This extension prefers to run on Workspace/Remote side but is running in local if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.remoteExtensionManagementServer) { - return nls.localize('enable remote', "Please reload Visual Studio Code to enable this extension in {0}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label); + return { action: reloadAction, reason: nls.localize('enable remote', "Please {0} to enable this extension in {1}.", reloadActionLabel, this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; } } } @@ -1150,20 +1345,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { // This extension prefers to run on UI/Local side but is running in remote if (this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } } if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { // This extension prefers to run on Workspace/Remote side but is running in local if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } } } return undefined; } else { if (isSameExtensionRunning) { - return nls.localize('postDisableTooltip', "Please reload Visual Studio Code to disable this extension."); + return { action: reloadAction, reason: nls.localize('postDisableTooltip', "Please {0} to disable this extension.", reloadActionLabel) }; } } return undefined; @@ -1172,7 +1367,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Extension is not running else { if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } const otherServer = extension.server ? extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; @@ -1180,7 +1375,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const extensionInOtherServer = this.local.filter(e => areSameExtensions(e.identifier, extension.identifier) && e.server === otherServer)[0]; // Same extension in other server exists and if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: reloadAction, reason: nls.localize('postEnableTooltip', "Please {0} to enable this extension.", reloadActionLabel) }; } } } @@ -1350,6 +1545,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Skip checking updates for a builtin extension if it is a system extension or if it does not has Marketplace identifier continue; } + if (installed.local?.source === 'resource') { + continue; + } infos.push({ ...installed.identifier, preRelease: !!installed.local?.preRelease }); } if (infos.length) { @@ -1357,15 +1555,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension type GalleryServiceUpdatesCheckClassification = { owner: 'sandy081'; comment: 'Report when a request is made to check for updates of extensions'; - readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extensions to check update' }; + count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extensions to check update'; isMeasurement: true }; }; type GalleryServiceUpdatesCheckEvent = { - readonly count: number; + count: number; }; this.telemetryService.publicLog2('galleryService:checkingForUpdates', { count: infos.length, }); - const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true }, CancellationToken.None); + const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true, productVersion: this.getProductVersion() }, CancellationToken.None); if (galleryExtensions.length) { await this.syncInstalledExtensionsWithGallery(galleryExtensions); } @@ -1403,8 +1601,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extensions.length) { return; } - const result = await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery))); - if (this.isAutoUpdateEnabled() && result.some(r => r.status === 'fulfilled' && r.value)) { + await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery, this.getProductVersion()))); + if (this.isAutoUpdateEnabled()) { this.eventuallyAutoUpdateExtensions(); } } @@ -1423,12 +1621,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private eventuallyCheckForUpdates(immediate = false): void { + this.updatesCheckDelayer.cancel(); this.updatesCheckDelayer.trigger(async () => { if (this.isAutoUpdateEnabled() || this.isAutoCheckUpdatesEnabled()) { await this.checkForUpdates(); } this.eventuallyCheckForUpdates(); - }, immediate ? 0 : ExtensionsWorkbenchService.UpdatesCheckInterval).then(undefined, err => null); + }, immediate ? 0 : this.getUpdatesCheckInterval()).then(undefined, err => null); + } + + private getUpdatesCheckInterval(): number { + if (this.productService.quality === 'insider' && this.getProductUpdateVersion()) { + return 1000 * 60 * 60 * 1; // 1 hour + } + return ExtensionsWorkbenchService.UpdatesCheckInterval; } private eventuallyAutoUpdateExtensions(): void { @@ -1463,8 +1669,35 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); + if (!toUpdate.length) { + return; + } - await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true } : undefined))); + const productVersion = this.getProductVersion(); + await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }))); + } + + private getProductVersion(): IProductVersion { + return this.getProductUpdateVersion() ?? this.getProductCurrentVersion(); + } + + private getProductCurrentVersion(): IProductVersion { + return { version: this.productService.version, date: this.productService.date }; + } + + private getProductUpdateVersion(): IProductVersion | undefined { + switch (this.updateService.state.type) { + case StateType.AvailableForDownload: + case StateType.Downloaded: + case StateType.Updating: + case StateType.Ready: { + const version = this.updateService.state.update.productVersion; + if (version && semver.valid(version)) { + return { version, date: this.updateService.state.update.timestamp ? new Date(this.updateService.state.update.timestamp).toISOString() : undefined }; + } + } + } + return undefined; } private async updateExtensionsPinnedState(): Promise { @@ -1626,40 +1859,149 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } - if (!extension.gallery) { - return false; - } + if (extension.gallery) { + if (this.localExtensions && await this.localExtensions.canInstall(extension.gallery)) { + return true; + } - if (this.localExtensions && await this.localExtensions.canInstall(extension.gallery)) { - return true; - } + if (this.remoteExtensions && await this.remoteExtensions.canInstall(extension.gallery)) { + return true; + } - if (this.remoteExtensions && await this.remoteExtensions.canInstall(extension.gallery)) { - return true; + if (this.webExtensions && await this.webExtensions.canInstall(extension.gallery)) { + return true; + } + return false; } - if (this.webExtensions && await this.webExtensions.canInstall(extension.gallery)) { + if (extension.resourceExtension) { return true; } return false; } + async install(arg: string | URI | IExtension, installOptions: InstallExtensionOptions = {}, progressLocation?: ProgressLocation): Promise { + let installable: URI | IGalleryExtension | IResourceExtension | undefined; + let extension: IExtension | undefined; - - install(extension: URI | IExtension, installOptions?: InstallOptions | InstallVSIXOptions, progressLocation?: ProgressLocation): Promise { - return this.doInstall(extension, async () => { - if (extension instanceof URI) { - return this.installFromVSIX(extension, installOptions); + if (arg instanceof URI) { + installable = arg; + } else { + let installableInfo: IExtensionInfo | undefined; + let gallery: IGalleryExtension | undefined; + if (isString(arg)) { + extension = this.local.find(e => areSameExtensions(e.identifier, { id: arg })); + if (!extension?.isBuiltin) { + installableInfo = { id: arg, version: installOptions.version, preRelease: installOptions.installPreReleaseVersion ?? this.preferPreReleases }; + } + } else if (arg.gallery) { + extension = arg; + gallery = arg.gallery; + if (installOptions.version && installOptions.version !== gallery?.version) { + installableInfo = { id: extension.identifier.id, version: installOptions.version }; + } + } else if (arg.resourceExtension) { + extension = arg; + installable = arg.resourceExtension; + } + if (installableInfo) { + const targetPlatform = extension?.server ? await extension.server.extensionManagementService.getTargetPlatform() : undefined; + gallery = firstOrDefault(await this.galleryService.getExtensions([installableInfo], { targetPlatform }, CancellationToken.None)); } - if (extension.isMalicious) { + if (!extension && gallery) { + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); + Extensions.updateExtensionFromControlManifest(extension as Extension, await this.extensionManagementService.getExtensionsControlManifest()); + } + if (extension?.isMalicious) { throw new Error(nls.localize('malicious', "This extension is reported to be problematic.")); } - if (!extension.gallery) { - throw new Error('Missing gallery'); + // Do not install if requested to enable and extension is already installed + if (!(installOptions.enable && extension?.local)) { + if (!installable) { + if (!gallery) { + const id = isString(arg) ? arg : (arg).identifier.id; + if (installOptions.version) { + throw new Error(nls.localize('not found version', "Unable to install extension '{0}' because the requested version '{1}' is not found.", id, installOptions.version)); + } else { + throw new Error(nls.localize('not found', "Unable to install extension '{0}' because it is not found.", id)); + } + } + installable = gallery; + } + if (installOptions.version) { + installOptions.installGivenVersion = true; + } + if (extension?.isWorkspaceScoped) { + installOptions.isWorkspaceScoped = true; + } + } + } + + if (installable) { + if (installOptions.justification) { + const syncCheck = isUndefined(installOptions.isMachineScoped) && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncEnablementService.isResourceEnabled(SyncResource.Extensions); + const buttons: IPromptButton[] = []; + buttons.push({ label: isString(installOptions.justification) ? nls.localize({ key: 'installButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Install Extension") : nls.localize({ key: 'installButtonLabelWithAction', comment: ['&& denotes a mnemonic'] }, "&&Install Extension and {0}", installOptions.justification.action), run: () => true }); + if (!extension) { + buttons.push({ label: nls.localize('open', "Open Extension"), run: () => { this.open(extension!); return false; } }); + } + const result = await this.dialogService.prompt({ + title: nls.localize('installExtensionTitle', "Install Extension"), + message: extension ? nls.localize('installExtensionMessage', "Would you like to install '{0}' extension from '{1}'?", extension.displayName, extension.publisherDisplayName) : nls.localize('installVSIXMessage', "Would you like to install the extension?"), + detail: isString(installOptions.justification) ? installOptions.justification : installOptions.justification.reason, + cancelButton: true, + buttons, + checkbox: syncCheck ? { + label: nls.localize('sync extension', "Sync this extension"), + checked: true, + } : undefined, + }); + if (!result.result) { + throw new CancellationError(); + } + if (syncCheck) { + installOptions.isMachineScoped = !result.checkboxChecked; + } } - return this.installFromGallery(extension, extension.gallery, installOptions); - }, progressLocation); + if (installable instanceof URI) { + extension = await this.doInstall(undefined, () => this.installFromVSIX(installable, installOptions), progressLocation); + } else if (extension) { + if (extension.resourceExtension) { + extension = await this.doInstall(extension, () => this.extensionManagementService.installResourceExtension(installable as IResourceExtension, installOptions), progressLocation); + } else { + extension = await this.doInstall(extension, () => this.installFromGallery(extension!, installable as IGalleryExtension, installOptions), progressLocation); + } + } + } + + if (!extension) { + throw new Error(nls.localize('unknown', "Unable to install extension")); + } + + if (installOptions.version) { + await this.updateAutoUpdateEnablementFor(extension, false); + } + + if (installOptions.enable) { + if (extension.enablementState === EnablementState.DisabledWorkspace || extension.enablementState === EnablementState.DisabledGlobally) { + if (installOptions.justification) { + const result = await this.dialogService.confirm({ + title: nls.localize('enableExtensionTitle', "Enable Extension"), + message: nls.localize('enableExtensionMessage', "Would you like to enable '{0}' extension?", extension.displayName), + detail: isString(installOptions.justification) ? installOptions.justification : installOptions.justification.reason, + primaryButton: isString(installOptions.justification) ? nls.localize({ key: 'enableButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Enable Extension") : nls.localize({ key: 'enableButtonLabelWithAction', comment: ['&& denotes a mnemonic'] }, "&&Enable Extension and {0}", installOptions.justification.action), + }); + if (!result.confirmed) { + throw new CancellationError(); + } + } + await this.setEnablement(extension, extension.enablementState === EnablementState.DisabledWorkspace ? EnablementState.EnabledWorkspace : EnablementState.EnabledGlobally); + } + await this.waitUntilExtensionIsEnabled(extension); + } + + return extension; } async installInServer(extension: IExtension, server: IExtensionManagementServer): Promise { @@ -1741,25 +2083,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }, () => this.extensionManagementService.uninstall(toUninstall).then(() => undefined)); } - async installVersion(extension: IExtension, version: string, installOptions: InstallOptions = {}): Promise { - extension = await this.doInstall(extension, async () => { - if (!extension.gallery) { - throw new Error('Missing gallery'); - } - - const targetPlatform = extension.server ? await extension.server.extensionManagementService.getTargetPlatform() : undefined; - const [gallery] = await this.galleryService.getExtensions([{ id: extension.gallery.identifier.id, version }], { targetPlatform }, CancellationToken.None); - if (!gallery) { - throw new Error(nls.localize('not found', "Unable to install extension '{0}' because the requested version '{1}' is not found.", extension.gallery.identifier.id, version)); - } - - installOptions.installGivenVersion = true; - return this.installFromGallery(extension, gallery, installOptions); - }); - await this.updateAutoUpdateEnablementFor(extension, false); - return extension; - } - reinstall(extension: IExtension): Promise { return this.doInstall(extension, () => { const ext = extension.local ? extension : this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; @@ -1826,21 +2149,21 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return extension; } - private doInstall(extension: IExtension | URI, installTask: () => Promise, progressLocation?: ProgressLocation): Promise { - const title = extension instanceof URI ? nls.localize('installing extension', 'Installing extension....') : nls.localize('installing named extension', "Installing '{0}' extension....", extension.displayName); + private doInstall(extension: IExtension | undefined, installTask: () => Promise, progressLocation?: ProgressLocation): Promise { + const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension....", extension.displayName) : nls.localize('installing extension', 'Installing extension....'); return this.withProgress({ location: progressLocation ?? ProgressLocation.Extensions, title }, async () => { try { - if (!(extension instanceof URI)) { + if (extension) { this.installing.push(extension); this._onChange.fire(extension); } const local = await installTask(); return await this.waitAndGetInstalledExtension(local.identifier); } finally { - if (!(extension instanceof URI)) { + if (extension) { this.installing = this.installing.filter(e => e !== extension); // Trigger the change without passing the extension because it is replaced by a new instance. this._onChange.fire(undefined); @@ -1849,7 +2172,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }); } - private async installFromVSIX(vsix: URI, installOptions?: InstallVSIXOptions): Promise { + private async installFromVSIX(vsix: URI, installOptions: InstallOptions): Promise { const manifest = await this.extensionManagementService.getManifest(vsix); const existingExtension = this.local.find(local => areSameExtensions(local.identifier, { id: getGalleryExtensionId(manifest.publisher, manifest.name) })); if (existingExtension) { @@ -1867,6 +2190,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installOptions = installOptions ?? {}; installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); if (extension.local) { + installOptions.productVersion = this.getProductVersion(); return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); } else { return this.extensionManagementService.installFromGallery(gallery, installOptions); @@ -1886,6 +2210,27 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return installedExtension; } + private async waitUntilExtensionIsEnabled(extension: IExtension): Promise { + if (this.extensionService.extensions.find(e => ExtensionIdentifier.equals(e.identifier, extension.identifier.id))) { + return; + } + if (!extension.local || !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { + return; + } + await new Promise((c, e) => { + const disposable = this.extensionService.onDidChangeExtensions(() => { + try { + if (this.extensionService.extensions.find(e => ExtensionIdentifier.equals(e.identifier, extension.identifier.id))) { + disposable.dispose(); + c(); + } + } catch (error) { + e(error); + } + }); + }); + } + private promptAndSetEnablement(extensions: IExtension[], enablementState: EnablementState): Promise { const enable = enablementState === EnablementState.EnabledGlobally || enablementState === EnablementState.EnabledWorkspace; if (enable) { diff --git a/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index c043223dc06..74ce489d01d 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { ExtensionRecommendations, GalleryExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -40,8 +40,8 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private readonly fileBasedRecommendations = new Map(); private readonly fileBasedImportantRecommendations = new Set(); - get recommendations(): ReadonlyArray { - const recommendations: ExtensionRecommendation[] = []; + get recommendations(): ReadonlyArray { + const recommendations: GalleryExtensionRecommendation[] = []; [...this.fileBasedRecommendations.keys()] .sort((a, b) => { if (this.fileBasedRecommendations.get(a)!.recommendedTime === this.fileBasedRecommendations.get(b)!.recommendedTime) { @@ -56,7 +56,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { }) .forEach(extensionId => { recommendations.push({ - extensionId, + extension: extensionId, reason: { reasonId: ExtensionRecommendationReason.File, reasonText: localize('fileBasedRecommendation', "This extension is recommended based on the files you recently opened.") @@ -66,12 +66,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return recommendations; } - get importantRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => this.fileBasedImportantRecommendations.has(e.extensionId)); + get importantRecommendations(): ReadonlyArray { + return this.recommendations.filter(e => this.fileBasedImportantRecommendations.has(e.extension)); } - get otherRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => !this.fileBasedImportantRecommendations.has(e.extensionId)); + get otherRecommendations(): ReadonlyArray { + return this.recommendations.filter(e => !this.fileBasedImportantRecommendations.has(e.extension)); } constructor( diff --git a/code/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index a40eed0f23f..3ad131168e5 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts @@ -21,7 +21,7 @@ export class KeymapRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.keymapExtensionTips) { this._recommendations = this.productService.keymapExtensionTips.map(extensionId => ({ - extensionId: extensionId.toLowerCase(), + extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: '' diff --git a/code/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts index 9258305b84a..ac493c927fe 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts @@ -21,7 +21,7 @@ export class LanguageRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.languageExtensionTips) { this._recommendations = this.productService.languageExtensionTips.map(extensionId => ({ - extensionId: extensionId.toLowerCase(), + extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: '' diff --git a/code/src/vs/workbench/contrib/extensions/browser/media/extension.css b/code/src/vs/workbench/contrib/extensions/browser/media/extension.css index 136ce1af0f7..985b5511c05 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/code/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -214,10 +214,6 @@ text-overflow: ellipsis; } -.extension-list-item > .details > .footer > .monaco-action-bar > .actions-container { - flex-wrap: wrap-reverse; -} - .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .action-label:not(.icon) { border-radius: 2px; } diff --git a/code/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/code/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 44f8b720581..59a07a33c6f 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/code/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -163,7 +163,7 @@ margin-left: 6px; } -.extension-editor > .header > .details > .subtitle > div:not(:first-child):not(:empty) { +.extension-editor > .header > .details > .subtitle > div:not(:first-child):not(:empty):not(.resource) { border-left: 1px solid rgba(128, 128, 128, 0.7); margin-left: 14px; padding-left: 14px; diff --git a/code/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts index 43d9c8a2dd8..f3ccbdd5a8b 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { ExtensionRecommendations, GalleryExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { PlatformToString, platform } from 'vs/base/common/platform'; export class RemoteRecommendations extends ExtensionRecommendations { - private _recommendations: ExtensionRecommendation[] = []; - get recommendations(): ReadonlyArray { return this._recommendations; } + private _recommendations: GalleryExtensionRecommendation[] = []; + get recommendations(): ReadonlyArray { return this._recommendations; } constructor( @IProductService private readonly productService: IProductService, @@ -23,7 +23,7 @@ export class RemoteRecommendations extends ExtensionRecommendations { const extensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips }; const currentPlatform = PlatformToString(platform); this._recommendations = Object.values(extensionTips).filter(({ supportedPlatforms }) => !supportedPlatforms || supportedPlatforms.includes(currentPlatform)).map(extension => ({ - extensionId: extension.extensionId.toLowerCase(), + extension: extension.extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: '' diff --git a/code/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts index 8688c14b824..bb72b0236d1 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts @@ -25,7 +25,7 @@ export class WebRecommendations extends ExtensionRecommendations { const isOnlyWeb = this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer; if (isOnlyWeb && Array.isArray(this.productService.webExtensionTips)) { this._recommendations = this.productService.webExtensionTips.map(extensionId => ({ - extensionId: extensionId.toLowerCase(), + extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: localize('reason', "This extension is recommended for {0} for the Web", this.productService.nameLong) diff --git a/code/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index e2e34e8f8fc..81b3d737aa5 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -4,13 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { distinct, flatten } from 'vs/base/common/arrays'; +import { distinct, equals, flatten } from 'vs/base/common/arrays'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { IExtensionsConfigContent, IWorkspaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; + +const WORKSPACE_EXTENSIONS_FOLDER = '.vscode/extensions'; export class WorkspaceRecommendations extends ExtensionRecommendations { @@ -23,16 +31,73 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { private _ignoredRecommendations: string[] = []; get ignoredRecommendations(): ReadonlyArray { return this._ignoredRecommendations; } + private workspaceExtensions: URI[] = []; + private readonly onDidChangeWorkspaceExtensionsScheduler: RunOnceScheduler; + constructor( @IWorkspaceExtensionsConfigService private readonly workspaceExtensionsConfigService: IWorkspaceExtensionsConfigService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileService private readonly fileService: IFileService, + @IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService, @INotificationService private readonly notificationService: INotificationService, ) { super(); + this.onDidChangeWorkspaceExtensionsScheduler = this._register(new RunOnceScheduler(() => this.onDidChangeWorkspaceExtensionsFolders(), 1000)); } protected async doActivate(): Promise { + this.workspaceExtensions = await this.fetchWorkspaceExtensions(); await this.fetch(); + this._register(this.workspaceExtensionsConfigService.onDidChangeExtensionsConfigs(() => this.onDidChangeExtensionsConfigs())); + for (const folder of this.contextService.getWorkspace().folders) { + this._register(this.fileService.watch(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER))); + } + + if (this.workbenchExtensionManagementService.isWorkspaceExtensionsSupported()) { + this._register(this.fileService.onDidFilesChange(e => { + if (this.contextService.getWorkspace().folders.some(folder => + e.affects(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER), FileChangeType.ADDED, FileChangeType.DELETED)) + ) { + this.onDidChangeWorkspaceExtensionsScheduler.schedule(); + } + })); + } + } + + private async onDidChangeWorkspaceExtensionsFolders(): Promise { + const existing = this.workspaceExtensions; + this.workspaceExtensions = await this.fetchWorkspaceExtensions(); + if (!equals(existing, this.workspaceExtensions, (a, b) => this.uriIdentityService.extUri.isEqual(a, b))) { + this.onDidChangeExtensionsConfigs(); + } + } + + private async fetchWorkspaceExtensions(): Promise { + if (!this.workbenchExtensionManagementService.isWorkspaceExtensionsSupported()) { + return []; + } + const workspaceExtensions: URI[] = []; + for (const workspaceFolder of this.contextService.getWorkspace().folders) { + const extensionsLocaiton = this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_EXTENSIONS_FOLDER); + try { + const stat = await this.fileService.resolve(extensionsLocaiton); + for (const extension of stat.children ?? []) { + if (!extension.isDirectory) { + continue; + } + workspaceExtensions.push(extension.resource); + } + } catch (error) { + // ignore + } + } + if (workspaceExtensions.length) { + const resourceExtensions = await this.workbenchExtensionManagementService.getExtensions(workspaceExtensions); + return resourceExtensions.map(extension => extension.location); + } + return []; } /** @@ -62,7 +127,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { for (const extensionId of extensionsConfig.recommendations) { if (invalidRecommendations.indexOf(extensionId) === -1) { this._recommendations.push({ - extensionId, + extension: extensionId, reason: { reasonId: ExtensionRecommendationReason.Workspace, reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.") @@ -72,6 +137,16 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } } + + for (const extension of this.workspaceExtensions) { + this._recommendations.push({ + extension, + reason: { + reasonId: ExtensionRecommendationReason.Workspace, + reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.") + } + }); + } } private async validateExtensions(contents: IExtensionsConfigContent[]): Promise<{ validRecommendations: string[]; invalidRecommendations: string[]; message: string }> { diff --git a/code/src/vs/workbench/contrib/extensions/common/extensions.ts b/code/src/vs/workbench/contrib/extensions/common/extensions.ts index 3e5d85b7bf2..737e37568bc 100644 --- a/code/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/code/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -6,8 +6,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { IPager } from 'vs/base/common/paging'; -import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { EnablementState, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EnablementState, IExtensionManagementServer, IResourceExtension } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -17,8 +17,8 @@ import { IView, IViewPaneContainer } from 'vs/workbench/common/views'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ProgressLocation } from 'vs/platform/progress/common/progress'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; export const VIEWLET_ID = 'workbench.view.extensions'; @@ -39,9 +39,20 @@ export const enum ExtensionState { Uninstalled } +export const enum ExtensionRuntimeActionType { + ReloadWindow = 'reloadWindow', + RestartExtensions = 'restartExtensions', + DownloadUpdate = 'downloadUpdate', + ApplyUpdate = 'applyUpdate', + QuitAndInstall = 'quitAndInstall', +} + +export type ExtensionRuntimeState = { action: ExtensionRuntimeActionType; reason: string }; + export interface IExtension { readonly type: ExtensionType; readonly isBuiltin: boolean; + readonly isWorkspaceScoped: boolean; readonly state: ExtensionState; readonly name: string; readonly displayName: string; @@ -69,7 +80,7 @@ export interface IExtension { readonly ratingCount?: number; readonly outdated: boolean; readonly outdatedTargetPlatform: boolean; - readonly reloadRequiredStatus?: string; + readonly runtimeState: ExtensionRuntimeState | undefined; readonly enablementState: EnablementState; readonly tags: readonly string[]; readonly categories: readonly string[]; @@ -85,12 +96,19 @@ export interface IExtension { readonly server?: IExtensionManagementServer; readonly local?: ILocalExtension; gallery?: IGalleryExtension; + readonly resourceExtension?: IResourceExtension; readonly isMalicious: boolean; readonly deprecationInfo?: IDeprecationInfo; } export const IExtensionsWorkbenchService = createDecorator('extensionsWorkbenchService'); +export interface InstallExtensionOptions extends InstallOptions { + version?: string; + justification?: string | { reason: string; action: string }; + enable?: boolean; +} + export interface IExtensionsWorkbenchService { readonly _serviceBrand: undefined; readonly onChange: Event; @@ -105,12 +123,13 @@ export interface IExtensionsWorkbenchService { queryGallery(options: IQueryOptions, token: CancellationToken): Promise>; getExtensions(extensionInfos: IExtensionInfo[], token: CancellationToken): Promise; getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise; + getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise; canInstall(extension: IExtension): Promise; - install(vsix: URI, installOptions?: InstallVSIXOptions): Promise; - install(extension: IExtension, installOptions?: InstallOptions, progressLocation?: ProgressLocation): Promise; + install(id: string, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; + install(vsix: URI, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; + install(extension: IExtension, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; installInServer(extension: IExtension, server: IExtensionManagementServer): Promise; uninstall(extension: IExtension): Promise; - installVersion(extension: IExtension, version: string, installOptions?: InstallOptions): Promise; reinstall(extension: IExtension): Promise; togglePreRelease(extension: IExtension): Promise; canSetLanguage(extension: IExtension): boolean; @@ -124,6 +143,7 @@ export interface IExtensionsWorkbenchService { checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; updateAll(): Promise; + updateRunningExtensions(): Promise; // Sync APIs isExtensionIgnoredToSync(extension: IExtension): boolean; diff --git a/code/src/vs/workbench/contrib/extensions/common/extensionsInput.ts b/code/src/vs/workbench/contrib/extensions/common/extensionsInput.ts index c8d1a1b1861..8215c4ef18d 100644 --- a/code/src/vs/workbench/contrib/extensions/common/extensionsInput.ts +++ b/code/src/vs/workbench/contrib/extensions/common/extensionsInput.ts @@ -34,7 +34,7 @@ export class ExtensionsInput extends EditorInput { } override get capabilities(): EditorInputCapabilities { - return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton | EditorInputCapabilities.AuxWindowUnsupported; + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; } override get resource() { diff --git a/code/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/code/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index a1304365806..ff915c04865 100644 --- a/code/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/code/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -28,6 +28,7 @@ import { RemoteExtensionsInitializerContribution } from 'vs/workbench/contrib/ex import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler'; +import { Disposable } from 'vs/base/common/lifecycle'; // Singletons registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, InstantiationType.Delayed); @@ -55,15 +56,18 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit // Global actions -class ExtensionsContributions implements IWorkbenchContribution { +class ExtensionsContributions extends Disposable implements IWorkbenchContribution { constructor( @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { + super(); + sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); - registerAction2(OpenExtensionsFolderAction); - registerAction2(CleanUpExtensionsFolderAction); + + this._register(registerAction2(OpenExtensionsFolderAction)); + this._register(registerAction2(CleanUpExtensionsFolderAction)); } } diff --git a/code/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/code/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 11dc035fb5b..14cb766b00d 100644 --- a/code/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/code/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -30,6 +30,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { IExtensionFeaturesManagementService } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -65,6 +66,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { private _profileSessionState: IContextKey; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -80,7 +82,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); + super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts index 4186a04ec35..8c96cf7a48e 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts @@ -27,78 +27,78 @@ suite('Extension Test', () => { }); test('extension is not outdated when there is no local and gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, undefined); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, undefined, undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when there is local and no gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), undefined); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), undefined, undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when there is no local and has gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, aGalleryExtension()); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, aGalleryExtension(), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local and gallery are on same version', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), aGalleryExtension()); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), aGalleryExtension(), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local is older than gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local is built in and older than gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is not outdated when local is built in and older than gallery but product quality is stable', () => { instantiationService.stub(IProductService, { quality: 'stable' }); - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local and gallery are on same version but on different target platforms', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_ARM64 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_ARM64 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is not outdated when local and gallery are on same version and local is on web', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), aGalleryExtension('somext')); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), aGalleryExtension('somext'), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local and gallery are on same version and gallery is on web', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext'), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WEB })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext'), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local is not pre-release but gallery is pre-release', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true }), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local and gallery are pre-releases', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local was opted to pre-release but current version is not pre-release', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local is pre-release but gallery is not', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local was opted pre-release but current version is not and gallery is not', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 1e9e11baa19..0e382e8c941 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -17,7 +17,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestProductService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestExtensionTipsService, TestSharedProcessService } from 'vs/workbench/test/electron-sandbox/workbenchTestServices'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -63,6 +63,11 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; + +const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -207,6 +212,13 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.stub(ILifecycleService, disposableStore.add(new TestLifecycleService())); testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); + instantiationService.stub(IProductService, TestProductService); + instantiationService.stub(ILogService, NullLogService); + const fileService = new FileService(instantiationService.get(ILogService)); + instantiationService.stub(IFileService, disposableStore.add(fileService)); + const fileSystemProvider = disposableStore.add(new InMemoryFileSystemProvider()); + disposableStore.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); + instantiationService.stub(IUriIdentityService, disposableStore.add(new UriIdentityService(instantiationService.get(IFileService)))); instantiationService.stub(INotificationService, new TestNotificationService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IWorkbenchExtensionManagementService, { @@ -219,7 +231,8 @@ suite('ExtensionRecommendationsService Test', () => { async getInstalled() { return []; }, async canInstall() { return true; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, - async getTargetPlatform() { return getTargetPlatform(platform, arch); } + async getTargetPlatform() { return getTargetPlatform(platform, arch); }, + isWorkspaceExtensionsSupported() { return false; }, }); instantiationService.stub(IExtensionService, { onDidChangeExtensions: Event.None, @@ -274,6 +287,7 @@ suite('ExtensionRecommendationsService Test', () => { }, }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); @@ -309,12 +323,7 @@ suite('ExtensionRecommendationsService Test', () => { } async function setUpFolder(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); - const logService = new NullLogService(); - const fileService = disposableStore.add(new FileService(logService)); - const fileSystemProvider = disposableStore.add(new InMemoryFileSystemProvider()); - disposableStore.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - + const fileService = instantiationService.get(IFileService); const folderDir = joinPath(ROOT, folderName); const workspaceSettingsDir = joinPath(folderDir, '.vscode'); await fileService.createFolder(workspaceSettingsDir); diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 0762678249d..ca20f73669d 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -56,6 +56,9 @@ import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/envi import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -78,6 +81,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(ILogService, NullLogService); instantiationService.stub(IWorkspaceContextService, new TestContextService()); + instantiationService.stub(IFileService, disposables.add(new FileService(new NullLogService()))); instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(IProductService, {}); @@ -94,6 +98,7 @@ function setupTest(disposables: Pick) { onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, async getInstalled() { return []; }, + async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, async updateMetadata(local: ILocalExtension, metadata: Partial) { local.identifier.uuid = metadata.id; @@ -136,6 +141,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } @@ -948,21 +954,21 @@ suite('ExtensionsActions', () => { }); -suite('ReloadAction', () => { +suite('ExtensionRuntimeStateAction', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(() => setupTest(disposables)); - test('Test ReloadAction when there is no extension', () => { - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + test('Test Runtime State when there is no extension', () => { + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension state is installing', async () => { - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + test('Test Runtime State when extension state is installing', async () => { + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const gallery = aGalleryExtension('a'); @@ -974,8 +980,8 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension state is uninstalling', async () => { - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + test('Test Runtime State when extension state is uninstalling', async () => { + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -986,7 +992,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is newly installed', async () => { + test('Test Runtime State when extension is newly installed', async () => { const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], @@ -994,7 +1000,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1008,10 +1014,10 @@ suite('ReloadAction', () => { didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); }); - test('Test ReloadAction when extension is newly installed and reload is not required', async () => { + test('Test Runtime State when extension is newly installed and ext host restart is not required', async () => { const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], @@ -1019,7 +1025,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => true, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1033,7 +1039,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is installed and uninstalled', async () => { + test('Test Runtime State when extension is installed and uninstalled', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1041,7 +1047,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1057,7 +1063,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is uninstalled', async () => { + test('Test Runtime State when extension is uninstalled', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], onDidChangeExtensions: Event.None, @@ -1066,7 +1072,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1076,10 +1082,10 @@ suite('ReloadAction', () => { uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to complete the uninstallation of this extension.'); + assert.strictEqual(testObject.tooltip, `Please restart extensions to complete the uninstallation of this extension.`); }); - test('Test ReloadAction when extension is uninstalled and can be removed', async () => { + test('Test Runtime State when extension is uninstalled and can be removed', async () => { const local = aLocalExtension('a'); instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(local)], @@ -1088,7 +1094,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => true, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); @@ -1099,7 +1105,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is uninstalled and installed', async () => { + test('Test Runtime State when extension is uninstalled and installed', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], onDidChangeExtensions: Event.None, @@ -1107,7 +1113,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1125,7 +1131,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is updated while running', async () => { + test('Test Runtime State when extension is updated while running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))], onDidChangeExtensions: Event.None, @@ -1134,7 +1140,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.1' }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1144,7 +1150,7 @@ suite('ReloadAction', () => { return new Promise(c => { disposables.add(testObject.onDidChange(() => { - if (testObject.enabled && testObject.tooltip === 'Please reload Visual Studio Code to enable the updated extension.') { + if (testObject.enabled && testObject.tooltip === `Please restart extensions to enable the updated extension.`) { c(); } })); @@ -1154,7 +1160,7 @@ suite('ReloadAction', () => { }); }); - test('Test ReloadAction when extension is updated when not running', async () => { + test('Test Runtime State when extension is updated when not running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1164,7 +1170,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1178,7 +1184,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is disabled when running', async () => { + test('Test Runtime State when extension is disabled when running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a'))], onDidChangeExtensions: Event.None, @@ -1187,7 +1193,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1198,10 +1204,10 @@ suite('ReloadAction', () => { await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to disable this extension.', testObject.tooltip); + assert.strictEqual(`Please restart extensions to disable this extension.`, testObject.tooltip); }); - test('Test ReloadAction when extension enablement is toggled when running', async () => { + test('Test Runtime State when extension enablement is toggled when running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], onDidChangeExtensions: Event.None, @@ -1210,7 +1216,7 @@ suite('ReloadAction', () => { whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1222,7 +1228,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is enabled when not running', async () => { + test('Test Runtime State when extension is enabled when not running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1232,7 +1238,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1241,10 +1247,10 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please restart extensions to enable this extension.`, testObject.tooltip); }); - test('Test ReloadAction when extension enablement is toggled when not running', async () => { + test('Test Runtime State when extension enablement is toggled when not running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1254,7 +1260,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1265,7 +1271,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is updated when not running and enabled', async () => { + test('Test Runtime State when extension is updated when not running and enabled', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a'))], onDidChangeExtensions: Event.None, @@ -1275,7 +1281,7 @@ suite('ReloadAction', () => { }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1288,10 +1294,10 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please restart extensions to enable this extension.`, testObject.tooltip); }); - test('Test ReloadAction when a localization extension is newly installed', async () => { + test('Test Runtime State when a localization extension is newly installed', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: Event.None, @@ -1299,7 +1305,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1313,7 +1319,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when a localization extension is updated while running', async () => { + test('Test Runtime State when a localization extension is updated while running', async () => { instantiationService.stub(IExtensionService, { extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))], onDidChangeExtensions: Event.None, @@ -1321,7 +1327,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a', { version: '1.0.1', contributes: { localizations: [{ languageId: 'de', translations: [] }] } }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1335,7 +1341,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is not installed but extension from different server is installed and running', async () => { + test('Test Runtime State when extension is not installed but extension from different server is installed and running', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a') }); @@ -1353,7 +1359,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1364,7 +1370,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when extension is uninstalled but extension from different server is installed and running', async () => { + test('Test Runtime State when extension is uninstalled but extension from different server is installed and running', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a') }); @@ -1387,7 +1393,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1403,7 +1409,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when workspace extension is disabled on local server and installed in remote server', async () => { + test('Test Runtime State when workspace extension is disabled on local server and installed in remote server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const remoteExtensionManagementService = createExtensionManagementService([]); @@ -1423,7 +1429,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1439,10 +1445,10 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload window to enable this extension.`); }); - test('Test ReloadAction when ui extension is disabled on remote server and installed in local server', async () => { + test('Test Runtime State when ui extension is disabled on remote server and installed in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtensionManagementService = createExtensionManagementService([]); @@ -1462,7 +1468,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1478,10 +1484,10 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload window to enable this extension.`); }); - test('Test ReloadAction for remote ui extension is disabled when it is installed and enabled in local server', async () => { + test('Test Runtime State for remote ui extension is disabled when it is installed and enabled in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file('pub.a') }); @@ -1502,7 +1508,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1513,7 +1519,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction for remote workspace+ui extension is enabled when it is installed and enabled in local server', async () => { + test('Test Runtime State for remote workspace+ui extension is enabled when it is installed and enabled in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file('pub.a') }); @@ -1534,7 +1540,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1545,7 +1551,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction for local ui+workspace extension is enabled when it is installed and enabled in remote server', async () => { + test('Test Runtime State for local ui+workspace extension is enabled when it is installed and enabled in remote server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file('pub.a') }); @@ -1566,7 +1572,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1577,7 +1583,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction for local workspace+ui extension is enabled when it is installed in both servers but running in local server', async () => { + test('Test Runtime State for local workspace+ui extension is enabled when it is installed in both servers but running in local server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file('pub.a') }); @@ -1598,7 +1604,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1609,7 +1615,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction for remote ui+workspace extension is enabled when it is installed on both servers but running in remote server', async () => { + test('Test Runtime State for remote ui+workspace extension is enabled when it is installed on both servers but running in remote server', async () => { // multi server setup const gallery = aGalleryExtension('a'); const localExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file('pub.a') }); @@ -1630,7 +1636,7 @@ suite('ReloadAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1641,7 +1647,7 @@ suite('ReloadAction', () => { assert.ok(testObject.enabled); }); - test('Test ReloadAction when ui+workspace+web extension is installed in web and remote and running in remote', async () => { + test('Test Runtime State when ui+workspace+web extension is installed in web and remote and running in remote', async () => { // multi server setup const gallery = aGalleryExtension('a'); const webExtension = aLocalExtension('a', { extensionKind: ['ui', 'workspace'], 'browser': 'browser.js' }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeUserData }) }); @@ -1658,7 +1664,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1669,7 +1675,7 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); }); - test('Test ReloadAction when workspace+ui+web extension is installed in web and local and running in local', async () => { + test('Test Runtime State when workspace+ui+web extension is installed in web and local and running in local', async () => { // multi server setup const gallery = aGalleryExtension('a'); const webExtension = aLocalExtension('a', { extensionKind: ['workspace', 'ui'], 'browser': 'browser.js' }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeUserData }) }); @@ -1686,7 +1692,7 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const testObject: ExtensionsActions.ReloadAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ReloadAction)); + const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 4a06d92c0e8..7ff4ea1b27a 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -48,6 +48,9 @@ import { arch } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; suite('ExtensionsViews Tests', () => { @@ -78,6 +81,7 @@ suite('ExtensionsViews Tests', () => { instantiationService = disposableStore.add(new TestInstantiationService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(ILogService, NullLogService); + instantiationService.stub(IFileService, disposableStore.add(new FileService(new NullLogService()))); instantiationService.stub(IProductService, {}); instantiationService.stub(IWorkspaceContextService, new TestContextService()); @@ -94,6 +98,7 @@ suite('ExtensionsViews Tests', () => { onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, async getInstalled() { return []; }, + async getInstalledWorkspaceExtensions() { return []; }, async canInstall() { return true; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, async getTargetPlatform() { return getTargetPlatform(platform, arch); }, @@ -187,6 +192,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); }); diff --git a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index ab080153b5e..e61f94d31bd 100644 --- a/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/code/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -51,6 +51,9 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Mutable } from 'vs/base/common/types'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -73,6 +76,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService = disposableStore.add(new TestInstantiationService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(ILogService, NullLogService); + instantiationService.stub(IFileService, disposableStore.add(new FileService(new NullLogService()))); instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(IProductService, {}); @@ -94,6 +98,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, async getInstalled() { return []; }, + async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, async updateMetadata(local: ILocalExtension, metadata: Partial) { local.identifier.uuid = metadata.id; @@ -131,6 +136,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', []); instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); }); test('test gallery extension', async () => { @@ -365,7 +371,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extension = page.firstPage[0]; assert.strictEqual(ExtensionState.Uninstalled, extension.state); - testObject.install(extension); const identifier = gallery.identifier; // Installing @@ -450,7 +455,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extension = page.firstPage[0]; assert.strictEqual(ExtensionState.Uninstalled, extension.state); - testObject.install(extension); installEvent.fire({ identifier: gallery.identifier, source: gallery }); const promise = Event.toPromise(testObject.onChange); @@ -470,7 +474,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extension = page.firstPage[0]; assert.strictEqual(ExtensionState.Uninstalled, extension.state); - testObject.install(extension); disposableStore.add(testObject.onChange(target)); // Installing diff --git a/code/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/code/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index ad722bacc27..1b6b279b308 100644 --- a/code/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/code/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -15,7 +15,7 @@ import { EditorResolution, IEditorOptions } from 'vs/platform/editor/common/edit import { IEditorResolverService, ResolvedStatus, ResolvedEditor } from 'vs/workbench/services/editor/common/editorResolverService'; import { isEditorInputWithOptions } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; /** * An implementation of editor for binary files that cannot be displayed. @@ -25,14 +25,15 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { static readonly ID = BINARY_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @IStorageService storageService: IStorageService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + @IStorageService storageService: IStorageService ) { super( BinaryFileEditor.ID, + group, { openInternal: (input, options) => this.openInternal(input, options) }, @@ -43,7 +44,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } private async openInternal(input: EditorInput, options: IEditorOptions | undefined): Promise { - if (input instanceof FileEditorInput && this.group?.activeEditor) { + if (input instanceof FileEditorInput && this.group.activeEditor) { // We operate on the active editor here to support re-opening // diff editors where `input` may just be one side of the @@ -84,7 +85,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } // Replace the active editor with the picked one - await (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + await this.group.replaceEditors([{ editor: activeEditor, replacement: resolvedEditor?.editor ?? input, options: { diff --git a/code/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/code/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index c0f1c912060..7c01103959b 100644 --- a/code/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/code/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -46,6 +46,7 @@ export class TextFileEditor extends AbstractTextCodeEditor static readonly ID = TEXT_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IFileService fileService: IFileService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -65,7 +66,7 @@ export class TextFileEditor extends AbstractTextCodeEditor @IHostService private readonly hostService: IHostService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(TextFileEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); // Clear view state for deleted files this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); @@ -192,7 +193,7 @@ export class TextFileEditor extends AbstractTextCodeEditor } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "The file is not displayed in the text editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -240,7 +241,6 @@ export class TextFileEditor extends AbstractTextCodeEditor private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { const defaultBinaryEditor = this.configurationService.getValue('workbench.editor.defaultBinaryEditor'); - const group = this.group ?? this.editorGroupService.activeGroup; const editorOptions = { ...options, @@ -259,9 +259,9 @@ export class TextFileEditor extends AbstractTextCodeEditor // and avoid enforcing binary or text on the file editor input. if (defaultBinaryEditor && defaultBinaryEditor !== '' && defaultBinaryEditor !== DEFAULT_EDITOR_ASSOCIATION.id) { - this.doOpenAsBinaryInDifferentEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInDifferentEditor(this.group, defaultBinaryEditor, input, editorOptions); } else { - this.doOpenAsBinaryInSameEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInSameEditor(this.group, defaultBinaryEditor, input, editorOptions); } } diff --git a/code/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/code/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 7d04ebe6d5d..066e0372c6b 100644 --- a/code/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/code/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -9,7 +9,7 @@ import { basename, isEqual } from 'vs/base/common/resources'; import { Action } from 'vs/base/common/actions'; import { URI } from 'vs/base/common/uri'; import { FileOperationError, FileOperationResult, IWriteFileOptions } from 'vs/platform/files/common/files'; -import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, ITextFileSaveAsOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, ITextFileSaveAsOptions, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -99,7 +99,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa } } - onSaveError(error: unknown, model: ITextFileEditorModel): void { + onSaveError(error: unknown, model: ITextFileEditorModel, options: ITextFileSaveOptions): void { const fileOperationError = error as FileOperationError; const resource = model.resource; @@ -127,7 +127,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa message = localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents or overwrite the content of the file with your changes.", basename(resource)); primaryActions.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model)); - primaryActions.push(this.instantiationService.createInstance(SaveModelIgnoreModifiedSinceAction, model)); + primaryActions.push(this.instantiationService.createInstance(SaveModelIgnoreModifiedSinceAction, model, options)); secondaryActions.push(this.instantiationService.createInstance(ConfigureSaveConflictAction)); } @@ -142,17 +142,17 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // Save Elevated if (canSaveElevated && (isPermissionDenied || triedToUnlock)) { - primaryActions.push(this.instantiationService.createInstance(SaveModelElevatedAction, model, !!triedToUnlock)); + primaryActions.push(this.instantiationService.createInstance(SaveModelElevatedAction, model, options, !!triedToUnlock)); } // Unlock else if (isWriteLocked) { - primaryActions.push(this.instantiationService.createInstance(UnlockModelAction, model)); + primaryActions.push(this.instantiationService.createInstance(UnlockModelAction, model, options)); } // Retry else { - primaryActions.push(this.instantiationService.createInstance(RetrySaveModelAction, model)); + primaryActions.push(this.instantiationService.createInstance(RetrySaveModelAction, model, options)); } // Save As @@ -272,6 +272,7 @@ class SaveModelElevatedAction extends Action { constructor( private model: ITextFileEditorModel, + private options: ITextFileSaveOptions, private triedToUnlock: boolean ) { super('workbench.files.action.saveModelElevated', triedToUnlock ? isWindows ? localize('overwriteElevated', "Overwrite as Admin...") : localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? localize('saveElevated', "Retry as Admin...") : localize('saveElevatedSudo', "Retry as Sudo...")); @@ -280,6 +281,7 @@ class SaveModelElevatedAction extends Action { override async run(): Promise { if (!this.model.isDisposed()) { await this.model.save({ + ...this.options, writeElevated: true, writeUnlock: this.triedToUnlock, reason: SaveReason.EXPLICIT @@ -291,14 +293,15 @@ class SaveModelElevatedAction extends Action { class RetrySaveModelAction extends Action { constructor( - private model: ITextFileEditorModel + private model: ITextFileEditorModel, + private options: ITextFileSaveOptions ) { super('workbench.files.action.saveModel', localize('retry', "Retry")); } override async run(): Promise { if (!this.model.isDisposed()) { - await this.model.save({ reason: SaveReason.EXPLICIT }); + await this.model.save({ ...this.options, reason: SaveReason.EXPLICIT }); } } } @@ -360,14 +363,15 @@ class SaveModelAsAction extends Action { class UnlockModelAction extends Action { constructor( - private model: ITextFileEditorModel + private model: ITextFileEditorModel, + private options: ITextFileSaveOptions ) { super('workbench.files.action.unlock', localize('overwrite', "Overwrite")); } override async run(): Promise { if (!this.model.isDisposed()) { - await this.model.save({ writeUnlock: true, reason: SaveReason.EXPLICIT }); + await this.model.save({ ...this.options, writeUnlock: true, reason: SaveReason.EXPLICIT }); } } } @@ -375,14 +379,15 @@ class UnlockModelAction extends Action { class SaveModelIgnoreModifiedSinceAction extends Action { constructor( - private model: ITextFileEditorModel + private model: ITextFileEditorModel, + private options: ITextFileSaveOptions ) { super('workbench.files.action.saveIgnoreModifiedSince', localize('overwrite', "Overwrite")); } override async run(): Promise { if (!this.model.isDisposed()) { - await this.model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); + await this.model.save({ ...this.options, ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); } } } diff --git a/code/src/vs/workbench/contrib/files/browser/files.contribution.ts b/code/src/vs/workbench/contrib/files/browser/files.contribution.ts index 80162ffd007..cc35374830e 100644 --- a/code/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/code/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -230,6 +230,12 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('trimTrailingWhitespace', "When enabled, will trim trailing whitespace when saving a file."), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE }, + 'files.trimTrailingWhitespaceInRegexAndStrings': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('trimTrailingWhitespaceInRegexAndStrings', "When enabled, trailing whitespace will be removed from multiline strings and regexes will be removed on save or when executing 'editor.action.trimTrailingWhitespace'. This can cause whitespace to not be trimmed from lines when there isn't up-to-date token information."), + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE + }, 'files.insertFinalNewline': { 'type': 'boolean', 'default': false, diff --git a/code/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/code/src/vs/workbench/contrib/files/browser/views/explorerView.ts index d7e695e1958..481d8354c7e 100644 --- a/code/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/code/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -273,10 +273,8 @@ export class ExplorerView extends ViewPane implements IExplorerView { const titleElement = container.querySelector('.title') as HTMLElement; const setHeader = () => { - const workspace = this.contextService.getWorkspace(); - const title = workspace.folders.map(folder => folder.name).join(); titleElement.textContent = this.name; - this.updateTitle(title); + this.updateTitle(this.name); this.ariaHeaderLabel = nls.localize('explorerSection', "Explorer Section: {0}", this.name); titleElement.setAttribute('aria-label', this.ariaHeaderLabel); }; diff --git a/code/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/code/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index f4d93f91e8d..ffe551566cb 100644 --- a/code/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/code/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -62,12 +62,13 @@ import { ResourceSet } from 'vs/base/common/map'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { timeout } from 'vs/base/common/async'; -import { IHoverDelegate, IHoverDelegateOptions, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { mainWindow } from 'vs/base/browser/window'; import { IExplorerFileContribution, explorerFileContribRegistry } from 'vs/workbench/contrib/files/browser/explorerFileContrib'; +import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; export class ExplorerDelegate implements IListVirtualDelegate { diff --git a/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index c6fd9a02c0d..ed7dcd5a06a 100644 --- a/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/code/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -26,7 +26,7 @@ import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAn import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableMap, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ResourceContextKey, MultipleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; @@ -118,7 +118,7 @@ export class OpenEditorsView extends ViewPane { this.listRefreshScheduler?.schedule(this.structuralRefreshDelay); }; - const groupDisposables = new Map(); + const groupDisposables = this._register(new DisposableMap()); const addGroupListener = (group: IEditorGroup) => { const groupModelChangeListener = group.onDidModelChange(e => { if (this.listRefreshScheduler?.isScheduled()) { @@ -156,7 +156,6 @@ export class OpenEditorsView extends ViewPane { } }); groupDisposables.set(group.id, groupModelChangeListener); - this._register(groupDisposables.get(group.id)!); }; this.editorGroupService.groups.forEach(g => addGroupListener(g)); @@ -167,7 +166,7 @@ export class OpenEditorsView extends ViewPane { this._register(this.editorGroupService.onDidMoveGroup(() => updateWholeList())); this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.focusActiveEditor())); this._register(this.editorGroupService.onDidRemoveGroup(group => { - dispose(groupDisposables.get(group.id)); + groupDisposables.deleteAndDispose(group.id); updateWholeList(); })); } diff --git a/code/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts b/code/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts index 94849cda9f7..7e99a23b0ed 100644 --- a/code/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts +++ b/code/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -22,7 +21,6 @@ export class DirtyFilesIndicator extends Disposable implements IWorkbenchContrib private lastKnownDirtyCount = 0; constructor( - @ILifecycleService private readonly lifecycleService: ILifecycleService, @IActivityService private readonly activityService: IActivityService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService @@ -38,9 +36,6 @@ export class DirtyFilesIndicator extends Disposable implements IWorkbenchContrib // Working copy dirty indicator this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.onWorkingCopyDidChangeDirty(workingCopy))); - - // Lifecycle - this.lifecycleService.onDidShutdown(() => this.dispose()); } private onWorkingCopyDidChangeDirty(workingCopy: IWorkingCopy): void { diff --git a/code/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/code/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index e1ecd72a649..f104ba762eb 100644 --- a/code/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/code/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -104,7 +104,7 @@ suite('EditorAutoSave', () => { assert.strictEqual(model.isDirty(), false); - await editorPane?.group?.closeAllEditors(); + await editorPane?.group.closeAllEditors(); }); function awaitModelSaved(model: ITextFileEditorModel): Promise { diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ea70b146a8c..c6a996a178c 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -51,8 +51,6 @@ registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.PreviousFromHistory); registerAction2(InlineChatActions.NextFromHistory); registerAction2(InlineChatActions.ViewInChatAction); -registerAction2(InlineChatActions.ExpandMessageAction); -registerAction2(InlineChatActions.ContractMessageAction); registerAction2(InlineChatActions.ToggleDiffForChange); registerAction2(InlineChatActions.FeebackHelpfulCommand); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 7d299bbc1e5..acab29a82f4 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -3,18 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .zone-widget.inline-chat-widget { +.monaco-workbench .zone-widget.inline-chat-widget { z-index: 3; } -.monaco-editor .zone-widget-container.inside-selection { +.monaco-workbench .zone-widget-container.inside-selection { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat { +.monaco-workbench .inline-chat { color: inherit; padding: 6px; - margin-top: 6px; border-radius: 6px; border: 1px solid var(--vscode-inlineChat-border); box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); @@ -23,23 +22,24 @@ /* body */ -.monaco-editor .inline-chat .body { +.monaco-workbench .inline-chat .body { display: flex; } -.monaco-editor .inline-chat .body .content { +.monaco-workbench .inline-chat-input { display: flex; box-sizing: border-box; outline: 1px solid var(--vscode-inlineChatInput-border); outline-offset: -1px; border-radius: 2px; + background-color: var(--vscode-inlineChatInput-background); } -.monaco-editor .inline-chat .body .content.synthetic-focus { +.monaco-workbench .inline-chat-input.synthetic-focus { outline: 1px solid var(--vscode-inlineChatInput-focusBorder); } -.monaco-editor .inline-chat .body .content .input { +.monaco-workbench .inline-chat-input .input { display: flex; align-items: center; justify-content: space-between; @@ -48,11 +48,11 @@ cursor: text; } -.monaco-editor .inline-chat .body .content .input .monaco-editor-background { +.monaco-workbench .inline-chat-input .input .monaco-editor-background { background-color: var(--vscode-inlineChatInput-background); } -.monaco-editor .inline-chat .body .content .input .editor-placeholder { +.monaco-workbench .inline-chat-input .input .editor-placeholder { position: absolute; z-index: 1; color: var(--vscode-inlineChatInput-placeholderForeground); @@ -61,64 +61,58 @@ text-overflow: ellipsis; } -.monaco-editor .inline-chat .body .content .input .editor-placeholder.hidden { +.monaco-workbench .inline-chat-input .input .editor-placeholder.hidden { display: none; } -.monaco-editor .inline-chat .body .content .input .editor-container { +.monaco-workbench .inline-chat-input .input .editor-container { vertical-align: middle; } -.monaco-editor .inline-chat .body .toolbar { + +.monaco-workbench .inline-chat-input .toolbar { display: flex; flex-direction: column; - align-self: stretch; + align-self: flex-start; + padding-top: 4px; padding-right: 4px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - background: var(--vscode-inlineChatInput-background); } -.monaco-editor .inline-chat .body .toolbar .actions-container { +.monaco-workbench .inline-chat-input .toolbar .actions-container { display: flex; flex-direction: row; gap: 4px; } -.monaco-editor .inline-chat .body > .widget-toolbar { +.monaco-workbench .inline-chat .body > .widget-toolbar { padding-left: 4px; + align-self: flex-start; } /* progress bit */ -.monaco-editor .inline-chat .progress { +.monaco-workbench .inline-chat .progress { position: relative; - width: calc(100% - 18px); - left: 19px; } /* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .monaco-editor .inline-chat .progress .monaco-progress-container { +.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { top: 0; } /* status */ -.monaco-editor .inline-chat .status { - margin-top: 4px; +.monaco-workbench .inline-chat .status { + padding-top: 4px; display: flex; justify-content: space-between; align-items: center; } -.monaco-editor .inline-chat .status.actions { - margin-top: 4px; -} - -.monaco-editor .inline-chat .status .actions.hidden { +.monaco-workbench .inline-chat .status .actions.hidden { display: none; } -.monaco-editor .inline-chat .status .label { +.monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); font-size: 11px; @@ -126,203 +120,179 @@ display: inline-flex; } -.monaco-editor .inline-chat .status .label.hidden { +.monaco-workbench .inline-chat .status .label.hidden { display: none; } -.monaco-editor .inline-chat .status .label.info { +.monaco-workbench .inline-chat .status .label.info { margin-right: auto; padding-left: 2px; } -.monaco-editor .inline-chat .status .label.info > .codicon { +.monaco-workbench .inline-chat .status .label.info > .codicon { padding: 0 5px; font-size: 12px; line-height: 18px; } -.monaco-editor .inline-chat .status .label.status { +.monaco-workbench .inline-chat .status .label.status { padding-left: 10px; padding-right: 4px; margin-left: auto; } -.monaco-editor .inline-chat .status .label .slash-command-pill CODE { +.monaco-workbench .inline-chat .status .label .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.monaco-editor .inline-chat .detectedIntent { +.monaco-workbench .inline-chat .detectedIntent { color: var(--vscode-descriptionForeground); padding: 5px 0px 5px 5px; } -.monaco-editor .inline-chat .detectedIntent.hidden { +.monaco-workbench .inline-chat .detectedIntent.hidden { display: none; } -.monaco-editor .inline-chat .detectedIntent .slash-command-pill CODE { +.monaco-workbench .inline-chat .detectedIntent .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.monaco-editor .inline-chat .detectedIntent .slash-command-pill a { +.monaco-workbench .inline-chat .detectedIntent .slash-command-pill a { color: var(--vscode-textLink-foreground); cursor: pointer; } -/* .monaco-editor .inline-chat .markdownMessage .message * { - margin: unset; -} - -.monaco-editor .inline-chat .markdownMessage .message code { - font-family: var(--monaco-monospace-font); - font-size: 12px; - color: var(--vscode-textPreformat-foreground); - background-color: var(--vscode-textPreformat-background); - padding: 1px 3px; - border-radius: 4px; -} */ - -.monaco-editor .inline-chat .chatMessage .chatMessageContent .value { - -webkit-line-clamp: initial; - -webkit-box-orient: vertical; +.monaco-workbench .inline-chat .chatMessage .chatMessageContent .value { overflow: hidden; - display: -webkit-box; -webkit-user-select: text; user-select: text; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); -} - -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); -} - -.monaco-editor .inline-chat .followUps { +.monaco-workbench .inline-chat .followUps { padding: 5px 5px; } -.monaco-editor .inline-chat .followUps .interactive-session-followups .monaco-button { +.monaco-workbench .inline-chat .followUps .interactive-session-followups .monaco-button { display: block; color: var(--vscode-textLink-foreground); font-size: 12px; } -.monaco-editor .inline-chat .followUps.hidden { +.monaco-workbench .inline-chat .followUps.hidden { display: none; } -.monaco-editor .inline-chat .chatMessage { - padding: 8px 3px; +.monaco-workbench .inline-chat .chatMessage { + padding: 0 3px; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent { +.monaco-workbench .inline-chat .chatMessage .chatMessageContent { padding: 2px 2px; } -.monaco-editor .inline-chat .chatMessage.hidden { +.monaco-workbench .inline-chat .chatMessage.hidden { display: none; } -.monaco-editor .inline-chat .status .label A { +.monaco-workbench .inline-chat .status .label A { color: var(--vscode-textLink-foreground); cursor: pointer; } -.monaco-editor .inline-chat .status .label.error { +.monaco-workbench .inline-chat .status .label.error { color: var(--vscode-errorForeground); } -.monaco-editor .inline-chat .status .label.warn { +.monaco-workbench .inline-chat .status .label.warn { color: var(--vscode-editorWarning-foreground); } -.monaco-editor .inline-chat .status .actions { +.monaco-workbench .inline-chat .status .actions { display: flex; } -.monaco-editor .inline-chat .status .actions > .monaco-button, -.monaco-editor .inline-chat .status .actions > .monaco-button-dropdown { +.monaco-workbench .inline-chat .status .actions > .monaco-button, +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown { margin-right: 6px; } -.monaco-editor .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { display: flex; align-items: center; padding: 0 4px; } -.monaco-editor .inline-chat .status .actions > .monaco-button.codicon { +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon { display: flex; } -.monaco-editor .inline-chat .status .actions > .monaco-button.codicon::before { +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon::before { align-self: center; } -.monaco-editor .inline-chat .status .actions .monaco-text-button { +.monaco-workbench .inline-chat .status .actions .monaco-text-button { padding: 2px 4px; white-space: nowrap; } -.monaco-editor .inline-chat .status .monaco-toolbar .action-item { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-item { padding: 0 2px; } /* TODO@jrieken not needed? */ -.monaco-editor .inline-chat .status .monaco-toolbar .action-label.checked { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); } -.monaco-editor .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { background-color: var(--vscode-button-hoverBackground); } /* preview */ -.monaco-editor .inline-chat .preview { +.monaco-workbench .inline-chat .preview { display: none; } -.monaco-editor .inline-chat .previewDiff, -.monaco-editor .inline-chat .previewCreate { +.monaco-workbench .inline-chat .previewDiff, +.monaco-workbench .inline-chat .previewCreate { display: inherit; border: 1px solid var(--vscode-inlineChat-border); border-radius: 2px; margin: 6px 0px; } -.monaco-editor .inline-chat .previewCreateTitle { +.monaco-workbench .inline-chat .previewCreateTitle { padding-top: 6px; } -.monaco-editor .inline-chat .diff-review.hidden, -.monaco-editor .inline-chat .previewDiff.hidden, -.monaco-editor .inline-chat .previewCreate.hidden, -.monaco-editor .inline-chat .previewCreateTitle.hidden { +.monaco-workbench .inline-chat .diff-review.hidden, +.monaco-workbench .inline-chat .previewDiff.hidden, +.monaco-workbench .inline-chat .previewCreate.hidden, +.monaco-workbench .inline-chat .previewCreateTitle.hidden { display: none; } -.monaco-editor .inline-chat-toolbar { +.monaco-workbench .inline-chat-toolbar { display: flex; } -.monaco-editor .inline-chat-toolbar > .monaco-button{ +.monaco-workbench .inline-chat-toolbar > .monaco-button { margin-right: 6px; } -.monaco-editor .inline-chat-toolbar .action-label.checked { +.monaco-workbench .inline-chat-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); @@ -330,65 +300,65 @@ /* decoration styles */ -.monaco-editor .inline-chat-inserted-range { +.monaco-workbench .inline-chat-inserted-range { background-color: var(--vscode-inlineChatDiff-inserted); } -.monaco-editor .inline-chat-inserted-range-linehighlight { +.monaco-workbench .inline-chat-inserted-range-linehighlight { background-color: var(--vscode-diffEditor-insertedLineBackground); } -.monaco-editor .inline-chat-original-zone2 { +.monaco-workbench .inline-chat-original-zone2 { background-color: var(--vscode-diffEditor-removedLineBackground); opacity: 0.8; } -.monaco-editor .inline-chat-lines-inserted-range { +.monaco-workbench .inline-chat-lines-inserted-range { background-color: var(--vscode-diffEditor-insertedTextBackground); } -.monaco-editor .inline-chat-block-selection { +.monaco-workbench .inline-chat-block-selection { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat-slash-command { +.monaco-workbench .inline-chat-slash-command { opacity: 0; } -.monaco-editor .inline-chat-slash-command-detail { +.monaco-workbench .inline-chat-slash-command-detail { opacity: 0.5; } /* diff zone */ -.monaco-editor .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, -.monaco-editor .inline-chat-diff-widget .monaco-diff-editor .monaco-editor .margin-view-overlays { +.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, +.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-workbench .margin-view-overlays { background-color: var(--vscode-inlineChat-regionHighlight); } /* create zone */ -.monaco-editor .inline-chat-newfile-widget { +.monaco-workbench .inline-chat-newfile-widget { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat-newfile-widget .title { +.monaco-workbench .inline-chat-newfile-widget .title { display: flex; align-items: center; justify-content: space-between; } -.monaco-editor .inline-chat-newfile-widget .title .detail { +.monaco-workbench .inline-chat-newfile-widget .title .detail { margin-left: 4px; } -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget { +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget { display: flex; margin-left: auto; margin-right: 8px; } -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { display: inline-flex; white-space: nowrap; margin-left: 4px; @@ -396,22 +366,22 @@ /* gutter decoration */ -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { display: block; cursor: pointer; transition: opacity .2s ease-in-out; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { opacity: 0.5; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { opacity: 0; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 75ca16a1f24..95e43b9d9d1 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -5,13 +5,13 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -31,11 +31,12 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ILogService } from 'vs/platform/log/common/log'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); -export const LOCALIZED_START_INLINE_CHAT_STRING = localize2('run', 'Start Inline Chat'); +export const LOCALIZED_START_INLINE_CHAT_STRING = localize2('run', 'Start in Editor'); export const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.')); // some gymnastics to enable hold for speech without moving the StartSessionAction into the electron-layer @@ -129,10 +130,28 @@ export abstract class AbstractInlineChatAction extends EditorAction2 { } override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { + const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + + let ctrl = InlineChatController.get(editor); + if (!ctrl) { + const { activeTextEditorControl } = editorService; + if (isCodeEditor(activeTextEditorControl)) { + editor = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + editor = activeTextEditorControl.getModifiedEditor(); + } + ctrl = InlineChatController.get(editor); + } + + if (!ctrl) { + logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); + return; + } + if (editor instanceof EmbeddedCodeEditorWidget) { editor = editor.getParentEditor(); } - const ctrl = InlineChatController.get(editor); if (!ctrl) { for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { @@ -737,46 +756,6 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class ExpandMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.expandMessageAction', - title: localize('expandMessage', 'Show More'), - icon: Codicon.chevronDown, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('cropped')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(true); - } -} - -export class ContractMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.contractMessageAction', - title: localize('contractMessage', 'Show Less'), - icon: Codicon.chevronUp, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('expanded')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(false); - } -} - export class InlineAccessibilityHelpContribution extends Disposable { constructor() { super(); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.css b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.css new file mode 100644 index 00000000000..dd658f7303d --- /dev/null +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.css @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .inline-chat-content-widget { + padding: 6px 6px 2px 6px; + border-radius: 4px; + background-color: var(--vscode-inlineChat-background); + box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); +} + +.monaco-workbench .inline-chat-content-widget .hidden { + display: none; +} + +.monaco-workbench .inline-chat-content-widget .message { + overflow: hidden; + color: var(--vscode-descriptionForeground); + font-size: 11px; + display: inline-flex; +} + +.monaco-workbench .inline-chat-content-widget .message > .codicon { + padding-right: 5px; + font-size: 12px; + line-height: 18px; +} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts new file mode 100644 index 00000000000..115385ab58d --- /dev/null +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./inlineChatContentWidget'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import * as dom from 'vs/base/browser/dom'; +import { IDimension } from 'vs/editor/common/core/dimension'; +import { Emitter, Event } from 'vs/base/common/event'; +import { InlineChatInputWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { clamp } from 'vs/base/common/numbers'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; + +export class InlineChatContentWidget implements IContentWidget { + + readonly suppressMouseDown = false; + readonly allowEditorOverflow = true; + + private readonly _store = new DisposableStore(); + private readonly _domNode = document.createElement('div'); + private readonly _inputContainer = document.createElement('div'); + private readonly _messageContainer = document.createElement('div'); + + private _position?: IPosition; + + private readonly _onDidBlur = this._store.add(new Emitter()); + readonly onDidBlur: Event = this._onDidBlur.event; + + private _visible: boolean = false; + private _focusNext: boolean = false; + + + constructor( + private readonly _editor: ICodeEditor, + private readonly _widget: InlineChatInputWidget, + ) { + this._store.add(this._widget.onDidChangeHeight(() => _editor.layoutContentWidget(this))); + + this._domNode.tabIndex = -1; + this._domNode.className = 'inline-chat-content-widget'; + + this._domNode.appendChild(this._inputContainer); + + this._messageContainer.classList.add('hidden', 'message'); + this._domNode.appendChild(this._messageContainer); + + this._widget.moveTo(this._inputContainer); + + const tracker = dom.trackFocus(this._domNode); + this._store.add(tracker.onDidBlur(() => { + if (this._visible) { + this._onDidBlur.fire(); + } + })); + this._store.add(tracker); + } + + dispose(): void { + this._store.dispose(); + } + + getId(): string { + return 'inline-chat-content-widget'; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IContentWidgetPosition | null { + if (!this._position) { + return null; + } + return { + position: this._position, + preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW] + }; + } + + beforeRender(): IDimension | null { + + const contentWidth = this._editor.getLayoutInfo().contentWidth; + const minWidth = contentWidth * 0.33; + const maxWidth = contentWidth * 0.66; + + const dim = this._widget.getPreferredSize(); + const width = clamp(dim.width, minWidth, maxWidth); + this._widget.layout(new dom.Dimension(width, dim.height)); + + return null; + } + + afterRender(): void { + if (this._focusNext) { + this._focusNext = false; + this._widget.focus(); + } + } + + // --- + + show(position: IPosition) { + if (!this._visible) { + this._visible = true; + this._focusNext = true; + + + this._widget.moveTo(this._inputContainer); + this._widget.reset(); + + const wordInfo = this._editor.getModel()?.getWordAtPosition(position); + + this._position = wordInfo ? new Position(position.lineNumber, wordInfo.startColumn) : position; + this._editor.addContentWidget(this); + } + } + + hide() { + if (this._visible) { + this._visible = false; + this._editor.removeContentWidget(this); + } + } + + updateMessage(message: string) { + this._messageContainer.classList.toggle('hidden', !message); + const renderedMessage = renderLabelWithIcons(message); + dom.reset(this._messageContainer, ...renderedMessage); + this._editor.layoutContentWidget(this); + } +} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 775b62fee68..07e276e48c4 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -33,20 +33,23 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { Progress } from 'vs/platform/progress/common/progress'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatSavingService } from './inlineChatSavingService'; -import { EmptyResponse, ErrorResponse, ExpansionState, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; -import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { IInlineChatMessageAppender } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; +import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; import { IValidEditOperation } from 'vs/editor/common/model'; +import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; +import { InlineChatHistory } from 'vs/workbench/contrib/inlineChat/browser/inlineChatHistory'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -105,15 +108,14 @@ export class InlineChatController implements IEditorContribution { return editor.getContribution(INLINE_CHAT_ID); } - private static _storageKey = 'inline-chat-history'; - private static _promptHistory: string[] = []; - private _historyOffset: number = -1; - private _historyCandidate: string = ''; - private _historyUpdate: (prompt: string) => void; + private readonly _history: InlineChatHistory; private _isDisposed: boolean = false; private readonly _store = new DisposableStore(); + private readonly _input: Lazy; private readonly _zone: Lazy; + + private readonly _ctxVisible: IContextKey; private readonly _ctxHasActiveRequest: IContextKey; private readonly _ctxResponseTypes: IContextKey; private readonly _ctxDidEdit: IContextKey; @@ -147,16 +149,18 @@ export class InlineChatController implements IEditorContribution { @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, - @IStorageService private readonly _storageService: IStorageService, @ICommandService private readonly _commandService: ICommandService, ) { + this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); this._ctxHasActiveRequest = CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.bindTo(contextKeyService); this._ctxDidEdit = CTX_INLINE_CHAT_DID_EDIT.bindTo(contextKeyService); this._ctxUserDidEdit = CTX_INLINE_CHAT_USER_DID_EDIT.bindTo(contextKeyService); this._ctxResponseTypes = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(contextKeyService); this._ctxLastFeedbackKind = CTX_INLINE_CHAT_LAST_FEEDBACK.bindTo(contextKeyService); this._ctxSupportIssueReporting = CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING.bindTo(contextKeyService); + this._zone = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatZoneWidget, this._editor))); + this._input = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatContentWidget, this._editor, this._zone.value.widget.inputWidget))); this._store.add(this._editor.onDidChangeModel(async e => { if (this._session || !e.newModelUrl) { @@ -191,17 +195,7 @@ export class InlineChatController implements IEditorContribution { this._log('NEW controller'); - InlineChatController._promptHistory = JSON.parse(_storageService.get(InlineChatController._storageKey, StorageScope.PROFILE, '[]')); - this._historyUpdate = (prompt: string) => { - const idx = InlineChatController._promptHistory.indexOf(prompt); - if (idx >= 0) { - InlineChatController._promptHistory.splice(idx, 1); - } - InlineChatController._promptHistory.unshift(prompt); - this._historyOffset = -1; - this._historyCandidate = ''; - this._storageService.store(InlineChatController._storageKey, JSON.stringify(InlineChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); - }; + this._history = _instaService.createInstance(InlineChatHistory, 'inline-chat-history'); } dispose(): void { @@ -251,8 +245,7 @@ export class InlineChatController implements IEditorContribution { if (options.initialSelection) { this._editor.setSelection(options.initialSelection); } - this._historyOffset = -1; - this._historyCandidate = ''; + this._history.clearCandidate(); this._stashedSession.clear(); this._onWillStartSession.fire(); this._currentRun = this._nextState(State.CREATE_SESSION, options); @@ -294,7 +287,7 @@ export class InlineChatController implements IEditorContribution { delete options.position; } - this._showWidget(true, initPosition); + const widgetPosition = this._showWidget(true, initPosition); this._updatePlaceholder(); @@ -333,7 +326,8 @@ export class InlineChatController implements IEditorContribution { delete options.existingSession; if (!session) { - this._dialogService.info(localize('create.fail', "Failed to start editor chat"), localize('create.fail.detail', "Please consult the error log and try again later.")); + MessageController.get(this._editor)?.showMessage(localize('create.fail', "Failed to start editor chat"), widgetPosition); + this._log('Failed to start editor chat'); return State.CANCEL; } @@ -376,10 +370,13 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.add(this._session.wholeRange.onDidChange(updateWholeRangeDecoration)); updateWholeRangeDecoration(); + this._sessionStore.add(this._input.value.onDidBlur(() => this.cancelSession())); + this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); - this._zone.value.widget.updateInfo(this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._zone.value.widget.preferredExpansionState = this._session.lastExpansionState; + const message = this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"); + this._input.value.updateMessage(message); + this._zone.value.widget.updateInfo(message); this._zone.value.widget.value = this._session.session.input ?? this._session.lastInput?.value ?? this._zone.value.widget.value; if (this._session.session.input) { this._zone.value.widget.selectAll(); @@ -508,8 +505,7 @@ export class InlineChatController implements IEditorContribution { const input = this.getInput(); - - this._historyUpdate(input); + this._history.update(input); const refer = this._session.session.slashCommands?.some(value => value.refer && input.startsWith(`/${value.command}`)); if (refer) { @@ -520,8 +516,8 @@ export class InlineChatController implements IEditorContribution { const withoutSubCommandLeader = input.slice(1); const cts = new CancellationTokenSource(); this._sessionStore.add(cts); - for (const agent of this._chatAgentService.getAgents()) { - const commands = await agent.provideSlashCommands(undefined, [], cts.token); + for (const agent of this._chatAgentService.getActivatedAgents()) { + const commands = agent.slashCommands; if (commands.find((command) => withoutSubCommandLeader.startsWith(command.name))) { massagedInput = `${chatAgentLeader}${agent.id} ${input}`; break; @@ -548,6 +544,8 @@ export class InlineChatController implements IEditorContribution { assertType(this._strategy); assertType(this._session.lastInput); + this._showWidget(false); + const requestCts = new CancellationTokenSource(); let message = Message.NONE; @@ -795,8 +793,6 @@ export class InlineChatController implements IEditorContribution { const message = { message: response.mdContent, providerId: this._session.provider.debugName, requestId: response.requestId }; this._zone.value.widget.updateChatMessage(message); - //this._zone.value.widget.updateMarkdownMessage(response.mdContent); - this._session.lastExpansionState = this._zone.value.widget.expansionState; this._zone.value.widget.updateToolbar(true); newPosition = await this._strategy.renderChanges(response); @@ -914,32 +910,38 @@ export class InlineChatController implements IEditorContribution { widgetPosition = this._editor.getSelection().getStartPosition().delta(-1); } - if (initialRender) { - this._zone.value.setContainerMargins(); - } - if (this._session && !position && (this._session.hasChangedText || this._session.lastExchange)) { widgetPosition = this._session.wholeRange.value.getStartPosition().delta(-1); } if (this._session) { this._zone.value.updateBackgroundColor(widgetPosition, this._session.wholeRange.value); } + if (!this._zone.value.position) { - this._zone.value.setWidgetMargins(widgetPosition); - this._zone.value.show(widgetPosition); + if (initialRender) { + widgetPosition = this._editor.getSelection().getStartPosition(); + // this._zone.value.hide(); + this._input.value.show(widgetPosition); + } else { + this._input.value.hide(); + this._zone.value.show(widgetPosition); + } } else { - this._zone.value.setWidgetMargins(widgetPosition); this._zone.value.updatePositionAndHeight(widgetPosition); } + this._ctxVisible.set(true); + return widgetPosition; } private _resetWidget() { this._sessionStore.clear(); + this._ctxVisible.reset(); this._ctxDidEdit.reset(); this._ctxUserDidEdit.reset(); this._ctxLastFeedbackKind.reset(); this._ctxSupportIssueReporting.reset(); + this._input.rawValue?.hide(); this._zone.rawValue?.hide(); // Return focus to the editor only if the current focus is within the editor widget @@ -1053,33 +1055,11 @@ export class InlineChatController implements IEditorContribution { } populateHistory(up: boolean) { - const len = InlineChatController._promptHistory.length; - if (len === 0) { - return; - } - - if (this._historyOffset === -1) { - // remember the current value - this._historyCandidate = this._zone.value.widget.value; - } - - const newIdx = this._historyOffset + (up ? 1 : -1); - if (newIdx >= len) { - // reached the end - return; - } - - let entry: string; - if (newIdx < 0) { - entry = this._historyCandidate; - this._historyOffset = -1; - } else { - entry = InlineChatController._promptHistory[newIdx]; - this._historyOffset = newIdx; + const entry = this._history.populateHistory(this._zone.value.widget.value, up); + if (entry) { + this._zone.value.widget.value = entry; + this._zone.value.widget.selectAll(); } - - this._zone.value.widget.value = entry; - this._zone.value.widget.selectAll(); } viewInChat() { @@ -1088,14 +1068,6 @@ export class InlineChatController implements IEditorContribution { } } - updateExpansionState(expand: boolean) { - if (this._session) { - const expansionState = expand ? ExpansionState.EXPANDED : ExpansionState.CROPPED; - this._zone.value.widget.updateChatMessageExpansionState(expansionState); - this._session.lastExpansionState = expansionState; - } - } - toggleDiff() { this._strategy?.toggleDiff?.(); } diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistory.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistory.ts new file mode 100644 index 00000000000..325e5f35d1d --- /dev/null +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistory.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +export class InlineChatHistory { + private _promptHistory: string[] = []; + private _historyOffset: number = -1; + private _historyCandidate: string = ''; + + constructor( + private readonly _storageKey: string, + @IStorageService private readonly _storageService: IStorageService, + ) { + this._promptHistory = JSON.parse(_storageService.get(this._storageKey, StorageScope.PROFILE, '[]')); + } + + update(prompt: string): void { + const idx = this._promptHistory.indexOf(prompt); + if (idx >= 0) { + this._promptHistory.splice(idx, 1); + } + this._promptHistory.unshift(prompt); + this._historyOffset = -1; + this._historyCandidate = ''; + this._storageService.store(this._storageKey, JSON.stringify(this._promptHistory), StorageScope.PROFILE, StorageTarget.USER); + } + + clearCandidate() { + this._historyOffset = -1; + this._historyCandidate = ''; + } + + populateHistory(currentValue: string, up: boolean): undefined | string { + const len = this._promptHistory.length; + if (len === 0) { + return undefined; + } + + if (this._historyOffset === -1) { + // remember the current value + this._historyCandidate = currentValue; + } + + const newIdx = this._historyOffset + (up ? 1 : -1); + if (newIdx >= len) { + // reached the end + return undefined; + } + + let entry: string; + if (newIdx < 0) { + entry = this._historyCandidate; + this._historyOffset = -1; + } else { + entry = this._promptHistory[newIdx]; + this._historyOffset = newIdx; + } + + return entry; + } +} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts new file mode 100644 index 00000000000..7743eb68f07 --- /dev/null +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts @@ -0,0 +1,407 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as aria from 'vs/base/browser/ui/aria/aria'; +import { Dimension, addDisposableListener, getTotalWidth, h, isAncestor } from 'vs/base/browser/dom'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; +import { Range } from 'vs/editor/common/core/range'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { LanguageSelector } from 'vs/editor/common/languageSelector'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { Position } from 'vs/editor/common/core/position'; +import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { localize } from 'vs/nls'; + + +export class InlineChatInputWidget { + + private readonly _elements = h( + 'div.inline-chat-input@content', + [ + h('div.input@input', [ + h('div.editor-placeholder@placeholder'), + h('div.editor-container@editor'), + ]), + h('div.toolbar@editorToolbar') + ] + ); + + private readonly _store = new DisposableStore(); + + private readonly _ctxInputEmpty: IContextKey; + private readonly _ctxInnerCursorFirst: IContextKey; + private readonly _ctxInnerCursorLast: IContextKey; + private readonly _ctxInnerCursorStart: IContextKey; + private readonly _ctxInnerCursorEnd: IContextKey; + private readonly _ctxInputEditorFocused: IContextKey; + + private readonly _inputEditor: IActiveCodeEditor; + private readonly _inputModel: ITextModel; + + private readonly _slashCommandContentWidget: SlashCommandContentWidget; + private readonly _slashCommands = this._store.add(new DisposableStore()); + private _slashCommandDetails: { command: string; detail: string }[] = []; + + protected readonly _onDidChangeHeight = this._store.add(new Emitter()); + readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; + + private readonly _onDidChangeInput = this._store.add(new Emitter()); + readonly onDidChangeInput: Event = this._onDidChangeInput.event; + + constructor( + options: { menuId: MenuId; telemetrySource: string; hoverDelegate: IHoverDelegate }, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IContextKeyService contextKeyService: IContextKeyService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + ) { + + this._inputEditor = instantiationService.createInstance(CodeEditorWidget, this._elements.editor, inputEditorOptions, codeEditorWidgetOptions); + this._store.add(this._inputEditor); + this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); + this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); + this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); + + const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${generateUuid()}.txt` }); + this._inputModel = this._store.add(modelService.getModel(uri) ?? modelService.createModel('', null, uri)); + this._inputEditor.setModel(this._inputModel); + + // placeholder + this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; + this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; + this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); + + // slash command content widget + this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor); + this._store.add(this._slashCommandContentWidget); + + // toolbar + this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, options.menuId, { + telemetrySource: options.telemetrySource, + toolbarOptions: { primaryGroup: 'main' }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu + hoverDelegate: options.hoverDelegate + })); + + + this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(contextKeyService); + this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(contextKeyService); + this._ctxInnerCursorLast = CTX_INLINE_CHAT_INNER_CURSOR_LAST.bindTo(contextKeyService); + this._ctxInnerCursorStart = CTX_INLINE_CHAT_INNER_CURSOR_START.bindTo(contextKeyService); + this._ctxInnerCursorEnd = CTX_INLINE_CHAT_INNER_CURSOR_END.bindTo(contextKeyService); + this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(contextKeyService); + + // (1) inner cursor position (last/first line selected) + const updateInnerCursorFirstLast = () => { + const selection = this._inputEditor.getSelection(); + const fullRange = this._inputModel.getFullModelRange(); + let onFirst = false; + let onLast = false; + if (selection.isEmpty()) { + const selectionTop = this._inputEditor.getTopForPosition(selection.startLineNumber, selection.startColumn); + const firstViewLineTop = this._inputEditor.getTopForPosition(fullRange.startLineNumber, fullRange.startColumn); + const lastViewLineTop = this._inputEditor.getTopForPosition(fullRange.endLineNumber, fullRange.endColumn); + + if (selectionTop === firstViewLineTop) { + onFirst = true; + } + if (selectionTop === lastViewLineTop) { + onLast = true; + } + } + this._ctxInnerCursorFirst.set(onFirst); + this._ctxInnerCursorLast.set(onLast); + this._ctxInnerCursorStart.set(fullRange.getStartPosition().equals(selection.getStartPosition())); + this._ctxInnerCursorEnd.set(fullRange.getEndPosition().equals(selection.getEndPosition())); + }; + this._store.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); + this._store.add(this._inputEditor.onDidChangeModelContent(updateInnerCursorFirstLast)); + updateInnerCursorFirstLast(); + + // (2) input editor focused or not + const updateFocused = () => { + const hasFocus = this._inputEditor.hasWidgetFocus(); + this._ctxInputEditorFocused.set(hasFocus); + this._elements.content.classList.toggle('synthetic-focus', hasFocus); + this.readPlaceholder(); + }; + this._store.add(this._inputEditor.onDidFocusEditorWidget(updateFocused)); + this._store.add(this._inputEditor.onDidBlurEditorWidget(updateFocused)); + this._store.add(toDisposable(() => { + this._ctxInnerCursorFirst.reset(); + this._ctxInnerCursorLast.reset(); + this._ctxInputEditorFocused.reset(); + })); + updateFocused(); + + + // show/hide placeholder depending on text model being empty + // content height + const currentContentHeight = 0; + const togglePlaceholder = () => { + const hasText = this._inputModel.getValueLength() > 0; + this._elements.placeholder.classList.toggle('hidden', hasText); + this._ctxInputEmpty.set(!hasText); + this.readPlaceholder(); + + const contentHeight = this._inputEditor.getContentHeight(); + if (contentHeight !== currentContentHeight) { + this._onDidChangeHeight.fire(); + } + }; + this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); + togglePlaceholder(); + } + + dispose(): void { + this.reset(); + this._store.dispose(); + } + + get domNode() { + return this._elements.content; + } + + moveTo(parent: HTMLElement) { + if (!isAncestor(this.domNode, parent)) { + parent.insertBefore(this.domNode, parent.firstChild); + } + } + + layout(dim: Dimension) { + const toolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; + const editorWidth = dim.width - toolbarWidth; + this._inputEditor.layout({ height: dim.height, width: editorWidth }); + this._elements.placeholder.style.width = `${editorWidth}px`; + } + + getPreferredSize(): Dimension { + const width = this._inputEditor.getContentWidth() + getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; + const height = this._inputEditor.getContentHeight(); + return new Dimension(width, height); + } + + getLineHeight(): number { + return this._inputEditor.getOption(EditorOption.lineHeight); + } + + reset() { + this._ctxInputEmpty.reset(); + this._ctxInnerCursorFirst.reset(); + this._ctxInnerCursorLast.reset(); + this._ctxInnerCursorStart.reset(); + this._ctxInnerCursorEnd.reset(); + this._ctxInputEditorFocused.reset(); + + this.value = ''; // update/re-inits some context keys again + } + + focus() { + this._inputEditor.focus(); + } + + get value(): string { + return this._inputModel.getValue(); + } + + set value(value: string) { + this._inputModel.setValue(value); + this._inputEditor.setPosition(this._inputModel.getFullModelRange().getEndPosition()); + } + + selectAll(includeSlashCommand: boolean = true) { + let selection = this._inputModel.getFullModelRange(); + + if (!includeSlashCommand) { + const firstLine = this._inputModel.getLineContent(1); + const slashCommand = this._slashCommandDetails.find(c => firstLine.startsWith(`/${c.command} `)); + selection = slashCommand ? new Range(1, slashCommand.command.length + 3, selection.endLineNumber, selection.endColumn) : selection; + } + + this._inputEditor.setSelection(selection); + } + + set ariaLabel(label: string) { + this._inputEditor.updateOptions({ ariaLabel: label }); + } + + set placeholder(value: string) { + this._elements.placeholder.innerText = value; + } + + readPlaceholder(): void { + const slashCommand = this._slashCommandDetails.find(c => `${c.command} ` === this._inputModel.getValue().substring(1)); + const hasText = this._inputModel.getValueLength() > 0; + if (!hasText) { + aria.status(this._elements.placeholder.innerText); + } else if (slashCommand) { + aria.status(slashCommand.detail); + } + } + + updateSlashCommands(commands: IInlineChatSlashCommand[]) { + + this._slashCommands.clear(); + this._slashCommandDetails = commands.filter(c => c.command && c.detail).map(c => { return { command: c.command, detail: c.detail! }; }); + + if (this._slashCommandDetails.length === 0) { + return; + } + + const selector: LanguageSelector = { scheme: this._inputModel.uri.scheme, pattern: this._inputModel.uri.path, language: this._inputModel.getLanguageId() }; + this._slashCommands.add(this._languageFeaturesService.completionProvider.register(selector, new class implements CompletionItemProvider { + + _debugDisplayName: string = 'InlineChatSlashCommandProvider'; + + readonly triggerCharacters?: string[] = ['/']; + + provideCompletionItems(_model: ITextModel, position: Position): ProviderResult { + if (position.lineNumber !== 1 && position.column !== 1) { + return undefined; + } + + const suggestions: CompletionItem[] = commands.map(command => { + + const withSlash = `/${command.command}`; + + return { + label: { label: withSlash, description: command.detail }, + insertText: `${withSlash} $0`, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + kind: CompletionItemKind.Text, + range: new Range(1, 1, 1, 1), + command: command.executeImmediately ? { id: 'inlineChat.accept', title: withSlash } : undefined + }; + }); + + return { suggestions }; + } + })); + + const decorations = this._inputEditor.createDecorationsCollection(); + + const updateSlashDecorations = () => { + this._slashCommandContentWidget.hide(); + // TODO@jrieken + // this._elements.detectedIntent.classList.toggle('hidden', true); + + const newDecorations: IModelDeltaDecoration[] = []; + for (const command of commands) { + const withSlash = `/${command.command}`; + const firstLine = this._inputModel.getLineContent(1); + if (firstLine.startsWith(withSlash)) { + newDecorations.push({ + range: new Range(1, 1, 1, withSlash.length + 1), + options: { + description: 'inline-chat-slash-command', + inlineClassName: 'inline-chat-slash-command', + after: { + // Force some space between slash command and placeholder + content: ' ' + } + } + }); + + this._slashCommandContentWidget.setCommandText(command.command); + this._slashCommandContentWidget.show(); + + // inject detail when otherwise empty + if (firstLine === `/${command.command}`) { + newDecorations.push({ + range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2), + options: { + description: 'inline-chat-slash-command-detail', + after: { + content: `${command.detail}`, + inlineClassName: 'inline-chat-slash-command-detail' + } + } + }); + } + break; + } + } + decorations.set(newDecorations); + }; + + this._slashCommands.add(this._inputEditor.onDidChangeModelContent(updateSlashDecorations)); + updateSlashDecorations(); + } +} + +export const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); + +export const inputEditorOptions: IEditorConstructionOptions = { + padding: { top: 2, bottom: 2 }, + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + hideCursorInOverviewRuler: true, + selectOnLineNumbers: false, + selectionHighlight: false, + scrollbar: { + useShadows: false, + vertical: 'hidden', + horizontal: 'auto', + alwaysConsumeMouseWheel: false + }, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + dragAndDrop: false, + revealHorizontalRightPadding: 5, + minimap: { enabled: false }, + guides: { indentation: false }, + rulers: [], + cursorWidth: 1, + cursorStyle: 'line', + cursorBlinking: 'blink', + wrappingStrategy: 'advanced', + wrappingIndent: 'none', + renderWhitespace: 'none', + dropIntoEditor: { enabled: true }, + quickSuggestions: false, + suggest: { + showIcons: false, + showSnippets: false, + showWords: true, + showStatusBar: false, + }, + wordWrap: 'on', + ariaLabel: defaultAriaLabel, + fontFamily: DEFAULT_FONT_FAMILY, + fontSize: 13, + lineHeight: 20 +}; + +export const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SnippetController2.ID, + SuggestController.ID + ]) +}; diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index a3e3865631d..109875de35f 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -67,11 +67,6 @@ export type TelemetryDataClassification = { responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; }; -export enum ExpansionState { - EXPANDED = 'expanded', - CROPPED = 'cropped', - NOT_CROPPED = 'not_cropped' -} export class SessionWholeRange { @@ -142,7 +137,6 @@ export class SessionWholeRange { export class Session { private _lastInput: SessionPrompt | undefined; - private _lastExpansionState: ExpansionState | undefined; private _isUnstashed: boolean = false; private readonly _exchange: SessionExchange[] = []; private readonly _startTime = new Date(); @@ -204,14 +198,6 @@ export class Session { this._isUnstashed = true; } - get lastExpansionState(): ExpansionState | undefined { - return this._lastExpansionState; - } - - set lastExpansionState(state: ExpansionState) { - this._lastExpansionState = state; - } - get textModelNSnapshotAltVersion(): number | undefined { return this._textModelNSnapshotAltVersion; } diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 30aa3f46b10..500d978c389 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -30,7 +30,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { InlineChatFileCreatePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget'; import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { HunkState } from './inlineChatSession'; import { assertType } from 'vs/base/common/types'; diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 084e20c5898..dff6caf62d9 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -5,39 +5,26 @@ import { Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import * as aria from 'vs/base/browser/ui/aria/aria'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; -import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; import { ISettableObservable, constObservable, derived, observableValue } from 'vs/base/common/observable'; -import { assertType } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; import 'vs/css!./inlineChat'; -import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; -import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; -import { EditorLayoutInfo, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; -import { LanguageSelector } from 'vs/editor/common/languageSelector'; -import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages'; -import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { IModelService } from 'vs/editor/common/services/model'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; -import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import { ITextModel } from 'vs/editor/common/model'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; @@ -51,70 +38,24 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { ResourceLabel } from 'vs/workbench/browser/labels'; -import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { ExpansionState, HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { ChatResponseViewModel, ChatViewModel, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; +import { HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_VISIBLE, IInlineChatFollowup, IInlineChatSlashCommand, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; - -const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); - -export const _inputEditorOptions: IEditorConstructionOptions = { - padding: { top: 2, bottom: 2 }, - overviewRulerLanes: 0, - glyphMargin: false, - lineNumbers: 'off', - folding: false, - hideCursorInOverviewRuler: true, - selectOnLineNumbers: false, - selectionHighlight: false, - scrollbar: { - useShadows: false, - vertical: 'hidden', - horizontal: 'auto', - alwaysConsumeMouseWheel: false - }, - lineDecorationsWidth: 0, - overviewRulerBorder: false, - scrollBeyondLastLine: false, - renderLineHighlight: 'none', - fixedOverflowWidgets: true, - dragAndDrop: false, - revealHorizontalRightPadding: 5, - minimap: { enabled: false }, - guides: { indentation: false }, - rulers: [], - cursorWidth: 1, - cursorStyle: 'line', - cursorBlinking: 'blink', - wrappingStrategy: 'advanced', - wrappingIndent: 'none', - renderWhitespace: 'none', - dropIntoEditor: { enabled: true }, - quickSuggestions: false, - suggest: { - showIcons: false, - showSnippets: false, - showWords: true, - showStatusBar: false, - }, - wordWrap: 'on', - ariaLabel: defaultAriaLabel, - fontFamily: DEFAULT_FONT_FAMILY, - fontSize: 13, - lineHeight: 20 -}; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { inputEditorOptions, codeEditorWidgetOptions, InlineChatInputWidget, defaultAriaLabel } from './inlineChatInputWidget'; + const _previewEditorEditorOptions: IDiffEditorConstructionOptions = { scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, @@ -136,9 +77,25 @@ export interface InlineChatWidgetViewState { } export interface IInlineChatWidgetConstructionOptions { - menuId: MenuId; + /** + * The telemetry source for all commands of this widget + */ + telemetrySource: string; + /** + * The menu that is inside the input editor, use for send, dictation + */ + inputMenuId: MenuId; + /** + * The menu that next to the input editor, use for close, config etc + */ widgetMenuId: MenuId; - statusMenuId: MenuId; + /** + * The menu that rendered as button bar, use for accept, discard etc + */ + statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions }; + /** + * The men that rendered in the lower right corner, use for feedback + */ feedbackMenuId: MenuId; } @@ -156,30 +113,19 @@ export interface IInlineChatMessageAppender { export class InlineChatWidget { - private static _modelPool: number = 1; - - private readonly _elements = h( + protected readonly _elements = h( 'div.inline-chat@root', [ - h('div.body', [ - h('div.content@content', [ - h('div.input@input', [ - h('div.editor-placeholder@placeholder'), - h('div.editor-container@editor'), - ]), - h('div.toolbar@editorToolbar'), - ]), + h('div.body@body', [ + h('div.content@content'), h('div.widget-toolbar@widgetToolbar') ]), h('div.progress@progress'), h('div.detectedIntent.hidden@detectedIntent'), h('div.previewDiff.hidden@previewDiff'), - h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), + h('div.previewCreateTitle.show-file-icons.hidden@previewCreateTitle'), h('div.previewCreate.hidden@previewCreate'), - h('div.chatMessage.hidden@chatMessage', [ - h('div.chatMessageContent@chatMessageContent'), - h('div.messageActions@messageActions') - ]), + h('div.chatMessage.hidden@chatMessage'), h('div.followUps.hidden@followUps'), h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ @@ -191,32 +137,19 @@ export class InlineChatWidget { ] ); - private readonly _store = new DisposableStore(); - private readonly _slashCommands = this._store.add(new DisposableStore()); - - private readonly _inputEditor: IActiveCodeEditor; - private readonly _inputModel: ITextModel; - private readonly _ctxInputEmpty: IContextKey; - private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>; - private readonly _ctxInnerCursorFirst: IContextKey; - private readonly _ctxInnerCursorLast: IContextKey; - private readonly _ctxInnerCursorStart: IContextKey; - private readonly _ctxInnerCursorEnd: IContextKey; - private readonly _ctxInputEditorFocused: IContextKey; - private readonly _ctxResponseFocused: IContextKey; + private readonly _chatMessageContents: HTMLDivElement; + private readonly _chatMessageScrollable: DomScrollableElement; - private readonly _progressBar: ProgressBar; + protected readonly _store = new DisposableStore(); - private readonly _previewDiffEditor: Lazy; - private readonly _previewDiffModel = this._store.add(new MutableDisposable()); + private readonly _inputWidget: InlineChatInputWidget; - private readonly _accessibleViewer = this._store.add(new MutableDisposable()); + private readonly _ctxResponseFocused: IContextKey; - private readonly _previewCreateTitle: ResourceLabel; - private readonly _previewCreateEditor: Lazy; - private readonly _previewCreateDispoable = this._store.add(new MutableDisposable()); + private readonly _progressBar: ProgressBar; - private readonly _onDidChangeHeight = this._store.add(new MicrotaskEmitter()); + + protected readonly _onDidChangeHeight = this._store.add(new MicrotaskEmitter()); readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); private readonly _onDidChangeLayout = this._store.add(new MicrotaskEmitter()); @@ -227,12 +160,9 @@ export class InlineChatWidget { readonly onRequestWithoutIntentDetection: Event = this._onRequestWithoutIntentDetection.event; private _lastDim: Dimension | undefined; + private _lastInputDim: Dimension | undefined; private _isLayouting: boolean = false; - private _preferredExpansionState: ExpansionState | undefined; - private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED; - private _slashCommandDetails: { command: string; detail: string }[] = []; - private _slashCommandContentWidget: SlashCommandContentWidget; private readonly _editorOptions: ChatEditorOptions; private _chatMessageDisposables = this._store.add(new DisposableStore()); @@ -240,175 +170,70 @@ export class InlineChatWidget { private _slashCommandUsedDisposables = this._store.add(new DisposableStore()); private _chatMessage: MarkdownString | undefined; + private readonly _codeBlockModelCollection: CodeBlockModelCollection; + private _responseViewModel: IChatResponseViewModel | undefined; constructor( - private readonly parentEditor: ICodeEditor, - _options: IInlineChatWidgetConstructionOptions, - @IModelService private readonly _modelService: IModelService, + options: IInlineChatWidgetConstructionOptions, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, @ILogService private readonly _logService: ILogService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, + @ITextModelService protected readonly _textModelResolverService: ITextModelService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { + // Share hover delegates between toolbars to support instant hover between both + const hoverDelegate = this._store.add(createInstantHoverDelegate()); // input editor logic - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - isSimpleWidget: true, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - SnippetController2.ID, - SuggestController.ID - ]) - }; - - this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this.parentEditor); - this._updateAriaLabel(); - this._store.add(this._inputEditor); - this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); - this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); - this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'focus', () => this._ctxResponseFocused.set(true))); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'blur', () => this._ctxResponseFocused.reset())); - - this._store.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._updateAriaLabel(); - } - })); + this._inputWidget = this._instantiationService.createInstance(InlineChatInputWidget, { menuId: options.inputMenuId, telemetrySource: options.telemetrySource, hoverDelegate }); + this._inputWidget.moveTo(this._elements.content); + this._store.add(this._inputWidget); + this._store.add(this._inputWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${InlineChatWidget._modelPool++}.txt` }); - this._inputModel = this._store.add(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); - this._inputEditor.setModel(this._inputModel); this._editorOptions = this._store.add(_instantiationService.createInstance(ChatEditorOptions, undefined, editorForeground, inputBackground, editorBackground)); + this._chatMessageContents = document.createElement('div'); + this._chatMessageContents.className = 'chatMessageContent'; + this._chatMessageContents.tabIndex = 0; + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageContents.style.maxHeight = `${this._inputWidget.getLineHeight() * 9}px`; - // --- context keys + this._chatMessageScrollable = new DomScrollableElement(this._chatMessageContents, { alwaysConsumeMouseWheel: true }); + this._store.add(this._chatMessageScrollable); + this._elements.chatMessage.appendChild(this._chatMessageScrollable.getDomNode()); + this._store.add(addDisposableListener(this._chatMessageContents, 'focus', () => this._ctxResponseFocused.set(true))); + this._store.add(addDisposableListener(this._chatMessageContents, 'blur', () => this._ctxResponseFocused.reset())); - this._ctxMessageCropState = CTX_INLINE_CHAT_MESSAGE_CROP_STATE.bindTo(this._contextKeyService); - this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(this._contextKeyService); - - this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); - this._ctxInnerCursorLast = CTX_INLINE_CHAT_INNER_CURSOR_LAST.bindTo(this._contextKeyService); - this._ctxInnerCursorStart = CTX_INLINE_CHAT_INNER_CURSOR_START.bindTo(this._contextKeyService); - this._ctxInnerCursorEnd = CTX_INLINE_CHAT_INNER_CURSOR_END.bindTo(this._contextKeyService); - this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this._contextKeyService); - this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); - - // (1) inner cursor position (last/first line selected) - const updateInnerCursorFirstLast = () => { - const selection = this._inputEditor.getSelection(); - const fullRange = this._inputModel.getFullModelRange(); - let onFirst = false; - let onLast = false; - if (selection.isEmpty()) { - const selectionTop = this._inputEditor.getTopForPosition(selection.startLineNumber, selection.startColumn); - const firstViewLineTop = this._inputEditor.getTopForPosition(fullRange.startLineNumber, fullRange.startColumn); - const lastViewLineTop = this._inputEditor.getTopForPosition(fullRange.endLineNumber, fullRange.endColumn); - - if (selectionTop === firstViewLineTop) { - onFirst = true; - } - if (selectionTop === lastViewLineTop) { - onLast = true; - } + this._store.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { + this._updateAriaLabel(); } - this._ctxInnerCursorFirst.set(onFirst); - this._ctxInnerCursorLast.set(onLast); - this._ctxInnerCursorStart.set(fullRange.getStartPosition().equals(selection.getStartPosition())); - this._ctxInnerCursorEnd.set(fullRange.getEndPosition().equals(selection.getEndPosition())); - }; - this._store.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); - updateInnerCursorFirstLast(); - - // (2) input editor focused or not - const updateFocused = () => { - const hasFocus = this._inputEditor.hasWidgetFocus(); - this._ctxInputEditorFocused.set(hasFocus); - this._elements.content.classList.toggle('synthetic-focus', hasFocus); - this.readPlaceholder(); - }; - this._store.add(this._inputEditor.onDidFocusEditorWidget(updateFocused)); - this._store.add(this._inputEditor.onDidBlurEditorWidget(updateFocused)); - this._store.add(toDisposable(() => { - this._ctxInnerCursorFirst.reset(); - this._ctxInnerCursorLast.reset(); - this._ctxInputEditorFocused.reset(); })); - updateFocused(); - - // placeholder - - this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; - this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; - this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); - - // show/hide placeholder depending on text model being empty - // content height - const currentContentHeight = 0; - - const togglePlaceholder = () => { - const hasText = this._inputModel.getValueLength() > 0; - this._elements.placeholder.classList.toggle('hidden', hasText); - this._ctxInputEmpty.set(!hasText); - this.readPlaceholder(); - - const contentHeight = this._inputEditor.getContentHeight(); - if (contentHeight !== currentContentHeight && this._lastDim) { - this._lastDim = this._lastDim.with(undefined, contentHeight); - this._inputEditor.layout(this._lastDim); - this._onDidChangeHeight.fire(); - } - }; - this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); - togglePlaceholder(); - - // slash command content widget - - this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor); - this._store.add(this._slashCommandContentWidget); - - // Share hover delegates between toolbars to support instant hover between both - const hoverDelegate = this._store.add(getDefaultHoverDelegate('element', true)); + // context keys + this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); // toolbars - - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, _options.menuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', - toolbarOptions: { primaryGroup: 'main' }, - hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu - hoverDelegate - })); - this._progressBar = new ProgressBar(this._elements.progress); this._store.add(this._progressBar); - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, options.widgetMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hoverDelegate })); - const workbenchMenubarOptions: IWorkbenchButtonBarOptions = { - telemetrySource: 'interactiveEditorWidget-toolbar', - buttonConfigProvider: action => { - if (action.id === ACTION_REGENERATE_RESPONSE) { - return { showIcon: true, showLabel: false, isSecondary: true }; - } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { - return { isSecondary: false }; - } else { - return { isSecondary: true }; - } - } - }; - const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, _options.statusMenuId, workbenchMenubarOptions); + + const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; + + const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); @@ -421,38 +246,26 @@ export class InlineChatWidget { } }; - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); + const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(feedbackToolbar); - // preview editors - this._previewDiffEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, { - useInlineViewWhenSpaceIsLimited: false, - ..._previewEditorEditorOptions, - onlyShowAccessibleDiffViewer: this._accessibilityService.isScreenReaderOptimized(), - }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor))); - - this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); - this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor))); - this._elements.chatMessageContent.tabIndex = 0; - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.tabIndex = 0; this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.statusLabel.tabIndex = 0; - const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions); - this._store.add(markdownMessageToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(markdownMessageToolbar); this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); } })); - } + // Code block rendering + this._codeBlockModelCollection = this._store.add(this._instantiationService.createInstance(CodeBlockModelCollection)); + } private _updateAriaLabel(): void { if (!this._accessibilityService.isScreenReaderOptimized()) { @@ -463,72 +276,65 @@ export class InlineChatWidget { const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); label = kbLabel ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); } - _inputEditorOptions.ariaLabel = label; - this._inputEditor.updateOptions({ ariaLabel: label }); + inputEditorOptions.ariaLabel = label; + this._inputWidget.ariaLabel = label; } dispose(): void { this._store.dispose(); - this._ctxInputEmpty.reset(); - this._ctxMessageCropState.reset(); } get domNode(): HTMLElement { return this._elements.root; } - layout(_dim: Dimension) { + layout(widgetDim: Dimension) { this._isLayouting = true; try { - if (this._accessibleViewer.value) { - this._accessibleViewer.value.width = _dim.width - 12; - } const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar); - const editorToolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; - const innerEditorWidth = _dim.width - editorToolbarWidth - widgetToolbarWidth; - const dim = new Dimension(innerEditorWidth, _dim.height); - if (!this._lastDim || !Dimension.equals(this._lastDim, dim)) { - this._lastDim = dim; - this._inputEditor.layout(new Dimension(innerEditorWidth, this._inputEditor.getContentHeight())); - this._elements.placeholder.style.width = `${innerEditorWidth /* input-padding*/}px`; - - if (this._previewDiffEditor.hasValue) { - const previewDiffDim = new Dimension(_dim.width - 12, Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight()))); - this._elements.previewDiff.style.width = `${previewDiffDim.width}px`; - this._elements.previewDiff.style.height = `${previewDiffDim.height}px`; - this._previewDiffEditor.value.layout(previewDiffDim); - } - - if (this._previewCreateEditor.hasValue) { - const previewCreateDim = new Dimension(dim.width, Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight()))); - this._previewCreateEditor.value.layout(previewCreateDim); - this._elements.previewCreate.style.height = `${previewCreateDim.height}px`; - } - - - const lineHeight = this.parentEditor.getOption(EditorOption.lineHeight); - const editorHeight = this.parentEditor.getLayoutInfo().height; - const editorHeightInLines = Math.floor(editorHeight / lineHeight); - this._elements.root.style.setProperty('--vscode-inline-chat-cropped', String(Math.floor(editorHeightInLines / 5))); - this._elements.root.style.setProperty('--vscode-inline-chat-expanded', String(Math.floor(editorHeightInLines / 3))); - this._onDidChangeLayout.fire(); + const innerEditorWidth = widgetDim.width - widgetToolbarWidth; + const inputDim = new Dimension(innerEditorWidth, this._inputWidget.getPreferredSize().height); + if (!this._lastDim || !Dimension.equals(this._lastDim, widgetDim) || !this._lastInputDim || !Dimension.equals(this._lastInputDim, inputDim)) { + this._lastDim = widgetDim; + this._lastInputDim = inputDim; + this._doLayout(widgetDim, inputDim); } } finally { + this._onDidChangeLayout.fire(); this._isLayouting = false; } } + getCodeBlockInfo(codeBlockIndex: number): Promise> | undefined { + if (!this._responseViewModel) { + return; + } + return this._codeBlockModelCollection.get(this._responseViewModel.sessionId, this._responseViewModel, codeBlockIndex); + } + + + protected _doLayout(widgetDimension: Dimension, inputDimension: Dimension): void { + this._elements.root.style.height = `${widgetDimension.height - this._getExtraHeight()}px`; + this._elements.root.style.width = `${widgetDimension.width}px`; + + this._elements.progress.style.width = `${inputDimension.width}px`; + this._chatMessageContents.style.width = `${widgetDimension.width - 10}px`; + + this._inputWidget.layout(inputDimension); + } + getHeight(): number { - const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status); - const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; + const editorHeight = this._inputWidget.getPreferredSize().height + 4 /*padding*/; + const progressHeight = getTotalHeight(this._elements.progress); const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent); + const chatResponseHeight = getTotalHeight(this._chatMessageContents); const followUpsHeight = getTotalHeight(this._elements.followUps); - const chatResponseHeight = getTotalHeight(this._elements.chatMessage); - const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0; - const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); - const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0; - const accessibleViewHeight = this._accessibleViewer.value?.height ?? 0; - return base + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + 18 /* padding */ + 8 /*shadow*/; + const statusHeight = getTotalHeight(this._elements.status); + return progressHeight + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + statusHeight + this._getExtraHeight(); + } + + private _getExtraHeight(): number { + return 12 /* padding */ + 2 /*border*/ + 12 /*shadow*/; } updateProgress(show: boolean) { @@ -541,39 +347,32 @@ export class InlineChatWidget { } } + get inputWidget(): InlineChatInputWidget { + return this._inputWidget; + } + + takeInputWidgetOwnership(): void { + this._inputWidget.moveTo(this._elements.content); + } + get value(): string { - return this._inputModel.getValue(); + return this._inputWidget.value; } set value(value: string) { - this._inputModel.setValue(value); - this._inputEditor.setPosition(this._inputModel.getFullModelRange().getEndPosition()); + this._inputWidget.value = value; } selectAll(includeSlashCommand: boolean = true) { - let selection = this._inputModel.getFullModelRange(); - - if (!includeSlashCommand) { - const firstLine = this._inputModel.getLineContent(1); - const slashCommand = this._slashCommandDetails.find(c => firstLine.startsWith(`/${c.command} `)); - selection = slashCommand ? new Range(1, slashCommand.command.length + 3, selection.endLineNumber, selection.endColumn) : selection; - } - - this._inputEditor.setSelection(selection); + this._inputWidget.selectAll(includeSlashCommand); } set placeholder(value: string) { - this._elements.placeholder.innerText = value; + this._inputWidget.placeholder = value; } readPlaceholder(): void { - const slashCommand = this._slashCommandDetails.find(c => `${c.command} ` === this._inputModel.getValue().substring(1)); - const hasText = this._inputModel.getValueLength() > 0; - if (!hasText) { - aria.status(this._elements.placeholder.innerText); - } else if (slashCommand) { - aria.status(slashCommand.detail); - } + this._inputWidget.readPlaceholder(); } updateToolbar(show: boolean) { @@ -584,67 +383,55 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - get expansionState(): ExpansionState { - return this._expansionState; - } - - set preferredExpansionState(expansionState: ExpansionState | undefined) { - this._preferredExpansionState = expansionState; - } - get responseContent(): string | undefined { return this._chatMessage?.value; } updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; updateChatMessage(message: IInlineChatMessage | undefined): void; - updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined { - let expansionState: ExpansionState; + updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined; + updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined { + this._chatMessageDisposables.clear(); + this._codeBlockModelCollection.clear(); + this._responseViewModel = undefined; this._chatMessage = message ? new MarkdownString(message.message.value) : undefined; const hasMessage = message?.message.value; this._elements.chatMessage.classList.toggle('hidden', !hasMessage); - reset(this._elements.chatMessageContent); + reset(this._chatMessageContents); let resultingAppender: IInlineChatMessageAppender | undefined; - if (!hasMessage) { - this._ctxMessageCropState.reset(); - expansionState = ExpansionState.NOT_CROPPED; - } else { - const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService)); + if (hasMessage) { + const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); - const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); - const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; + this._responseViewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); + const chatViewModel = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatViewModel, sessionModel, this._codeBlockModelCollection)); + chatViewModel.updateCodeBlockTextModels(this._responseViewModel); + const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true, editableCodeBlock: isCodeBlockEditable ?? false }; const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; - const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, undefined)); - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); // 2 for the padding used for the tab index border + const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, this._codeBlockModelCollection, undefined)); + renderer.layout(this._chatMessageContents.clientWidth - 4); // 2 for the padding used for the tab index border this._chatMessageDisposables.add(this._onDidChangeLayout.event(() => { - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); + renderer.layout(this._chatMessageContents.clientWidth - 4); + this._chatMessageScrollable.scanDomNode(); })); - const template = renderer.renderTemplate(this._elements.chatMessageContent); + const template = renderer.renderTemplate(this._chatMessageContents); this._chatMessageDisposables.add(template.elementDisposables); this._chatMessageDisposables.add(template.templateDisposables); - renderer.renderChatTreeItem(viewModel, 0, template); + renderer.renderChatTreeItem(this._responseViewModel, 0, template); this._chatMessageDisposables.add(renderer.onDidChangeItemHeight(() => this._onDidChangeHeight.fire())); - if (this._preferredExpansionState) { - expansionState = this._preferredExpansionState; - this._preferredExpansionState = undefined; - } else { - this._updateLineClamp(ExpansionState.CROPPED); - expansionState = template.value.scrollHeight > template.value.clientHeight ? ExpansionState.CROPPED : ExpansionState.NOT_CROPPED; - } - this._ctxMessageCropState.set(expansionState); - this._updateLineClamp(expansionState); resultingAppender = isIncomplete ? { cancel: () => responseModel.cancel(), complete: () => responseModel.complete(), appendContent: (fragment: string) => { responseModel.updateContent({ kind: 'markdownContent', content: new MarkdownString(fragment) }); this._chatMessage?.appendMarkdown(fragment); + renderer.layout(this._chatMessageContents.clientWidth - 4); + this._chatMessageScrollable.scanDomNode(); + this._onDidChangeHeight.fire(); } } : undefined; } - this._expansionState = expansionState; this._onDidChangeHeight.fire(); return resultingAppender; } @@ -662,23 +449,15 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - updateChatMessageExpansionState(expansionState: ExpansionState) { - this._ctxMessageCropState.set(expansionState); - const heightBefore = this._elements.chatMessageContent.scrollHeight; - this._updateLineClamp(expansionState); - const heightAfter = this._elements.chatMessageContent.scrollHeight; - if (heightBefore === heightAfter) { - this._ctxMessageCropState.set(ExpansionState.NOT_CROPPED); - } - this._onDidChangeHeight.fire(); - } + private _currentSlashCommands: IInlineChatSlashCommand[] = []; - private _updateLineClamp(expansionState: ExpansionState) { - this._elements.chatMessageContent.setAttribute('state', expansionState); + updateSlashCommands(commands: IInlineChatSlashCommand[]) { + this._currentSlashCommands = commands; + this._inputWidget.updateSlashCommands(commands); } updateSlashCommandUsed(command: string): void { - const details = this._slashCommandDetails.find(candidate => candidate.command === command); + const details = this._currentSlashCommands.find(candidate => candidate.command === command); if (!details) { return; } @@ -736,12 +515,7 @@ export class InlineChatWidget { } reset() { - this._ctxInputEmpty.reset(); - this._ctxInnerCursorFirst.reset(); - this._ctxInnerCursorLast.reset(); - this._ctxInputEditorFocused.reset(); - - this.value = ''; + this._inputWidget.reset(); this.updateChatMessage(undefined); this.updateFollowUps(undefined); @@ -751,23 +525,115 @@ export class InlineChatWidget { this._elements.statusToolbar.classList.add('hidden'); this._elements.feedbackToolbar.classList.add('hidden'); this.updateInfo(''); - this.hideCreatePreview(); - this.hideEditsPreview(); - this._accessibleViewer.clear(); this._elements.accessibleViewer.classList.toggle('hidden', true); - this._onDidChangeHeight.fire(); } focus() { - this._inputEditor.focus(); + this._inputWidget.focus(); } hasFocus() { return this.domNode.contains(getActiveElement()); } +} + +export class EditorBasedInlineChatWidget extends InlineChatWidget { + + private readonly _accessibleViewer = this._store.add(new MutableDisposable()); + + private readonly _previewDiffEditor: Lazy; + private readonly _previewDiffModel = this._store.add(new MutableDisposable()); + + private readonly _previewCreateTitle: ResourceLabel; + private readonly _previewCreateEditor: Lazy; + private readonly _previewCreateDispoable = this._store.add(new MutableDisposable()); + + constructor( + private readonly _parentEditor: ICodeEditor, + options: IInlineChatWidgetConstructionOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IConfigurationService configurationService: IConfigurationService, + @IAccessibleViewService accessibleViewService: IAccessibleViewService, + @ILogService logService: ILogService, + @ITextModelService textModelResolverService: ITextModelService, + @IChatAgentService chatAgentService: IChatAgentService, + ) { + super(options, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, logService, textModelResolverService, chatAgentService,); + + // preview editors + this._previewDiffEditor = new Lazy(() => this._store.add(instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, { + useInlineViewWhenSpaceIsLimited: false, + ..._previewEditorEditorOptions, + onlyShowAccessibleDiffViewer: accessibilityService.isScreenReaderOptimized(), + }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, _parentEditor))); + + this._previewCreateTitle = this._store.add(instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); + this._previewCreateEditor = new Lazy(() => this._store.add(instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, _parentEditor))); + } + + // --- layout + + override getHeight(): number { + const result = super.getHeight(); + const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0; + const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); + const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0; + const accessibleViewHeight = this._accessibleViewer.value?.height ?? 0; + return result + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight; + } + + protected override _doLayout(widgetDimension: Dimension, inputDimension: Dimension): void { + super._doLayout(widgetDimension, inputDimension); + + if (this._accessibleViewer.value) { + this._accessibleViewer.value.width = widgetDimension.width - 12; + } + + if (this._previewDiffEditor.hasValue) { + const previewDiffDim = new Dimension(widgetDimension.width - 12, Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight()))); + this._elements.previewDiff.style.width = `${previewDiffDim.width}px`; + this._elements.previewDiff.style.height = `${previewDiffDim.height}px`; + this._previewDiffEditor.value.layout(previewDiffDim); + } + + if (this._previewCreateEditor.hasValue) { + const previewCreateDim = new Dimension(inputDimension.width, Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight()))); + this._previewCreateEditor.value.layout(previewCreateDim); + this._elements.previewCreate.style.height = `${previewCreateDim.height}px`; + } + } + + override reset() { + this.hideCreatePreview(); + this.hideEditsPreview(); + this._accessibleViewer.clear(); + super.reset(); + } + + // --- accessible viewer + + showAccessibleHunk(session: Session, hunkData: HunkInformation): void { + + this._elements.accessibleViewer.classList.remove('hidden'); + this._accessibleViewer.clear(); + + this._accessibleViewer.value = this._instantiationService.createInstance(HunkAccessibleDiffViewer, + this._elements.accessibleViewer, + session, + hunkData, + new AccessibleHunk(this._parentEditor, session, hunkData) + ); + + this._onDidChangeHeight.fire(); + + } + // --- preview showEditsPreview(hunks: HunkData, textModel0: ITextModel, textModelN: ITextModel) { @@ -840,269 +706,6 @@ export class InlineChatWidget { return !this._elements.previewDiff.classList.contains('hidden') || !this._elements.previewCreate.classList.contains('hidden'); } - - // --- slash commands - - updateSlashCommands(commands: IInlineChatSlashCommand[]) { - - this._slashCommands.clear(); - - if (commands.length === 0) { - return; - } - this._slashCommandDetails = commands.filter(c => c.command && c.detail).map(c => { return { command: c.command, detail: c.detail! }; }); - - const selector: LanguageSelector = { scheme: this._inputModel.uri.scheme, pattern: this._inputModel.uri.path, language: this._inputModel.getLanguageId() }; - this._slashCommands.add(this._languageFeaturesService.completionProvider.register(selector, new class implements CompletionItemProvider { - - _debugDisplayName: string = 'InlineChatSlashCommandProvider'; - - readonly triggerCharacters?: string[] = ['/']; - - provideCompletionItems(_model: ITextModel, position: Position): ProviderResult { - if (position.lineNumber !== 1 && position.column !== 1) { - return undefined; - } - - const suggestions: CompletionItem[] = commands.map(command => { - - const withSlash = `/${command.command}`; - - return { - label: { label: withSlash, description: command.detail }, - insertText: `${withSlash} $0`, - insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, - kind: CompletionItemKind.Text, - range: new Range(1, 1, 1, 1), - command: command.executeImmediately ? { id: 'inlineChat.accept', title: withSlash } : undefined - }; - }); - - return { suggestions }; - } - })); - - const decorations = this._inputEditor.createDecorationsCollection(); - - const updateSlashDecorations = () => { - this._slashCommandContentWidget.hide(); - this._elements.detectedIntent.classList.toggle('hidden', true); - - const newDecorations: IModelDeltaDecoration[] = []; - for (const command of commands) { - const withSlash = `/${command.command}`; - const firstLine = this._inputModel.getLineContent(1); - if (firstLine.startsWith(withSlash)) { - newDecorations.push({ - range: new Range(1, 1, 1, withSlash.length + 1), - options: { - description: 'inline-chat-slash-command', - inlineClassName: 'inline-chat-slash-command', - after: { - // Force some space between slash command and placeholder - content: ' ' - } - } - }); - - this._slashCommandContentWidget.setCommandText(command.command); - this._slashCommandContentWidget.show(); - - // inject detail when otherwise empty - if (firstLine === `/${command.command}`) { - newDecorations.push({ - range: new Range(1, withSlash.length + 1, 1, withSlash.length + 2), - options: { - description: 'inline-chat-slash-command-detail', - after: { - content: `${command.detail}`, - inlineClassName: 'inline-chat-slash-command-detail' - } - } - }); - } - break; - } - } - decorations.set(newDecorations); - }; - - this._slashCommands.add(this._inputEditor.onDidChangeModelContent(updateSlashDecorations)); - updateSlashDecorations(); - } - - - // --- accessible viewer - - showAccessibleHunk(session: Session, hunkData: HunkInformation): void { - - this._elements.accessibleViewer.classList.remove('hidden'); - this._accessibleViewer.clear(); - - this._accessibleViewer.value = this._instantiationService.createInstance(HunkAccessibleDiffViewer, - this._elements.accessibleViewer, - session, - hunkData, - new AccessibleHunk(this.parentEditor, session, hunkData) - ); - - this._onDidChangeHeight.fire(); - - } -} - -export class InlineChatZoneWidget extends ZoneWidget { - - readonly widget: InlineChatWidget; - - private readonly _ctxVisible: IContextKey; - private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - private _dimension?: Dimension; - private _indentationWidth: number | undefined; - - constructor( - editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); - - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); - - this._disposables.add(toDisposable(() => { - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - })); - - this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { - menuId: MENU_INLINE_CHAT_INPUT, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, - statusMenuId: MENU_INLINE_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK - }); - this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); - this._disposables.add(this.widget); - this.create(); - - - this._disposables.add(addDisposableListener(this.domNode, 'click', e => { - if (!this.widget.hasFocus()) { - this.widget.focus(); - } - }, true)); - - // todo@jrieken listen ONLY when showing - const updateCursorIsAboveContextKey = () => { - if (!this.position || !this.editor.hasModel()) { - this._ctxCursorPosition.reset(); - } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('above'); - } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('below'); - } else { - this._ctxCursorPosition.reset(); - } - }; - this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); - this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); - updateCursorIsAboveContextKey(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this.widget.domNode); - } - - - protected override _doLayout(heightInPixel: number): void { - - const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; - const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); - this._dimension = new Dimension(width, heightInPixel); - this.widget.domNode.style.width = `${width}px`; - this.widget.layout(this._dimension); - } - - private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { - const info = this.editor.getLayoutInfo(); - return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); - } - - private _computeHeightInLines(): number { - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - return this.widget.getHeight() / lineHeight; - } - - protected override _relayout() { - if (this._dimension) { - this._doLayout(this._dimension.height); - } - super._relayout(this._computeHeightInLines()); - } - - override show(position: Position): void { - super.show(position, this._computeHeightInLines()); - this.widget.focus(); - this._ctxVisible.set(true); - } - - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; - } - - updateBackgroundColor(newPosition: Position, wholeRange: IRange) { - assertType(this.container); - const widgetLineNumber = newPosition.lineNumber; - this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); - } - - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; - } - const visibleRange = viewModel.getCompletelyVisibleViewRange(); - const startLineVisibleRange = visibleRange.startLineNumber; - const positionLine = position.lineNumber; - let indentationLineNumber: number | undefined; - let indentationLevel: number | undefined; - for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { - const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); - if (currentIndentationLevel !== 0) { - indentationLineNumber = lineNumber; - indentationLevel = currentIndentationLevel; - break; - } - } - return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); - } - - setContainerMargins(): void { - assertType(this.container); - - const info = this.editor.getLayoutInfo(); - const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; - this.container.style.marginLeft = `${marginWithoutIndentation}px`; - } - - setWidgetMargins(position: Position): void { - const indentationWidth = this._calculateIndentationWidth(position); - if (this._indentationWidth === indentationWidth) { - return; - } - this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; - this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; - this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; - } - - override hide(): void { - this.container!.classList.remove('inside-selection'); - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - this.widget.reset(); - super.hide(); - aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); - } } class HunkAccessibleDiffViewer extends AccessibleDiffViewer { diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts new file mode 100644 index 00000000000..808e2181b48 --- /dev/null +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Dimension, addDisposableListener } from 'vs/base/browser/dom'; +import * as aria from 'vs/base/browser/ui/aria/aria'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import { localize } from 'vs/nls'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditorBasedInlineChatWidget } from './inlineChatWidget'; + + +export class InlineChatZoneWidget extends ZoneWidget { + + readonly widget: EditorBasedInlineChatWidget; + + private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; + private _dimension?: Dimension; + private _indentationWidth: number | undefined; + + constructor( + editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); + + this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); + + this._disposables.add(toDisposable(() => { + this._ctxCursorPosition.reset(); + })); + + this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, this.editor, { + telemetrySource: 'interactiveEditorWidget-toolbar', + inputMenuId: MENU_INLINE_CHAT_INPUT, + widgetMenuId: MENU_INLINE_CHAT_WIDGET, + feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK, + statusMenuId: { + menu: MENU_INLINE_CHAT_WIDGET_STATUS, + options: { + buttonConfigProvider: action => { + if (action.id === ACTION_REGENERATE_RESPONSE) { + return { showIcon: true, showLabel: false, isSecondary: true }; + } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { + return { isSecondary: false }; + } else { + return { isSecondary: true }; + } + } + } + } + }); + this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); + this._disposables.add(this.widget); + this.create(); + + + this._disposables.add(addDisposableListener(this.domNode, 'click', e => { + if (!this.widget.hasFocus()) { + this.widget.focus(); + } + }, true)); + + // todo@jrieken listen ONLY when showing + const updateCursorIsAboveContextKey = () => { + if (!this.position || !this.editor.hasModel()) { + this._ctxCursorPosition.reset(); + } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('above'); + } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('below'); + } else { + this._ctxCursorPosition.reset(); + } + }; + this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); + this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); + updateCursorIsAboveContextKey(); + } + + protected override _fillContainer(container: HTMLElement): void { + container.appendChild(this.widget.domNode); + } + + + protected override _doLayout(heightInPixel: number): void { + const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; + const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); + this._dimension = new Dimension(width, heightInPixel); + this.widget.layout(this._dimension); + } + + private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { + const info = this.editor.getLayoutInfo(); + return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); + } + + private _computeHeightInLines(): number { + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + const widgetHeight = this.widget.getHeight(); + return widgetHeight / lineHeight; + } + + protected override _onWidth(_widthInPixel: number): void { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + } + + protected override _relayout() { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + super._relayout(this._computeHeightInLines()); + } + + override show(position: Position): void { + assertType(this.container); + + const info = this.editor.getLayoutInfo(); + const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; + this.container.style.marginLeft = `${marginWithoutIndentation}px`; + + this._setWidgetMargins(position); + this.widget.takeInputWidgetOwnership(); + super.show(position, this._computeHeightInLines()); + this.widget.focus(); + } + + override updatePositionAndHeight(position: Position): void { + this._setWidgetMargins(position); + super.updatePositionAndHeight(position); + } + + protected override _getWidth(info: EditorLayoutInfo): number { + return info.width - info.minimap.minimapWidth; + } + + updateBackgroundColor(newPosition: Position, wholeRange: IRange) { + assertType(this.container); + const widgetLineNumber = newPosition.lineNumber; + this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); + } + + private _calculateIndentationWidth(position: Position): number { + const viewModel = this.editor._getViewModel(); + if (!viewModel) { + return 0; + } + const visibleRange = viewModel.getCompletelyVisibleViewRange(); + const startLineVisibleRange = visibleRange.startLineNumber; + const positionLine = position.lineNumber; + let indentationLineNumber: number | undefined; + let indentationLevel: number | undefined; + for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { + const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); + if (currentIndentationLevel !== 0) { + indentationLineNumber = lineNumber; + indentationLevel = currentIndentationLevel; + break; + } + } + return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); + } + + private _setWidgetMargins(position: Position): void { + const indentationWidth = this._calculateIndentationWidth(position); + if (this._indentationWidth === indentationWidth) { + return; + } + this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; + this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; + this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; + } + + override hide(): void { + this.container!.classList.remove('inside-selection'); + this._ctxCursorPosition.reset(); + this.widget.reset(); + super.hide(); + aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); + } +} diff --git a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 10e6097adea..d44c8e11d1e 100644 --- a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -156,7 +156,6 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('in export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); -export const CTX_INLINE_CHAT_MESSAGE_CROP_STATE = new RawContextKey<'cropped' | 'not_cropped' | 'expanded'>('inlineChatMarkdownMessageCropState', 'not_cropped', localize('inlineChatMarkdownMessageCropState', "Whether the interactive editor message is cropped, not cropped or expanded")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); export const CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('inlineChatHasActiveRequest', false, localize('inlineChatHasActiveRequest', "Whether interactive editor has an active request")); export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); @@ -181,7 +180,6 @@ export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; export const MENU_INLINE_CHAT_INPUT = MenuId.for('inlineChatInput'); export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); -export const MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE = MenuId.for('inlineChatWidget.markdownMessage'); export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); export const MENU_INLINE_CHAT_WIDGET_FEEDBACK = MenuId.for('inlineChatWidget.feedback'); export const MENU_INLINE_CHAT_WIDGET_DISCARD = MenuId.for('inlineChatWidget.undo'); diff --git a/code/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts b/code/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts index b75bb758c6c..7f24f989723 100644 --- a/code/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts +++ b/code/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts @@ -226,7 +226,7 @@ export class InlineChatQuickVoice implements IEditorContribution { this._store.dispose(); } - start() { + async start() { this._finishCallback?.(true); @@ -236,7 +236,7 @@ export class InlineChatQuickVoice implements IEditorContribution { let message: string | undefined; let preview: string | undefined; - const session = this._voiceChatService.createVoiceChatSession(cts.token, { usesAgents: false }); + const session = await this._voiceChatService.createVoiceChatSession(cts.token, { usesAgents: false }); const listener = session.onDidChange(e => { if (cts.token.isCancellationRequested) { diff --git a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 6fd5722bc72..276c86cb858 100644 --- a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -44,6 +44,10 @@ import { TestWorkerService } from './testWorkerService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { Schemas } from 'vs/base/common/network'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; suite('InteractiveChatController', function () { class TestController extends InlineChatController { @@ -113,6 +117,9 @@ suite('InteractiveChatController', function () { const serviceCollection = new ServiceCollection( [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], + [IChatContributionService, new MockChatContributionService( + [{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])], + [IChatAgentService, new SyncDescriptor(ChatAgentService)], [IInlineChatService, inlineChatService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], @@ -146,6 +153,14 @@ suite('InteractiveChatController', function () { ); instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + const chatAgentService = instaService.get(IChatAgentService); + const agent = { + async invoke(request, progress, history, token) { + return {}; + }, + } satisfies IChatAgentImplementation; + store.add(chatAgentService.registerAgent('testAgent', agent)); + inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); diff --git a/code/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/code/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 6d300714340..6ff1ed9b726 100644 --- a/code/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/code/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -50,7 +50,7 @@ import { INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/no import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; -import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -253,9 +253,13 @@ class InteractiveWindowWorkingCopyEditorHandler extends Disposable implements IW } } -registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); +registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); type interactiveEditorInputData = { resource: URI; inputResource: URI; name: string; language: string }; @@ -793,3 +797,16 @@ Registry.as(ConfigurationExtensions.Configuration).regis } } }); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'interactiveWindow', + order: 100, + type: 'object', + 'properties': { + [NotebookSetting.InteractiveWindowPromptToSave]: { + type: 'boolean', + default: false, + markdownDescription: localize('interactiveWindow.promptToSaveOnClose', "Prompt to save the interactive window when it is closed. Only new interactive windows will be affected by this setting change.") + } + } +}); diff --git a/code/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/code/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index af62b50a84c..95e4406ce2e 100644 --- a/code/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/code/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; @@ -19,7 +19,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; +import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling } from 'vs/workbench/common/editor'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput'; import { ICellViewModel, INotebookEditorOptions, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -63,7 +63,6 @@ import { INTERACTIVE_WINDOW_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; -import { mainWindow } from 'vs/base/browser/window'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -81,7 +80,7 @@ export interface InteractiveEditorOptions extends ITextEditorOptions { readonly viewState?: InteractiveEditorViewState; } -export class InteractiveEditor extends EditorPane { +export class InteractiveEditor extends EditorPane implements IEditorPaneWithScrolling { private _rootElement!: HTMLElement; private _styleElement!: HTMLStyleElement; private _notebookEditorContainer!: HTMLElement; @@ -108,15 +107,18 @@ export class InteractiveEditor extends EditorPane { private _editorOptions: IEditorOptions; private _notebookOptions: NotebookOptions; private _editorMemento: IEditorMemento; - private _groupListener = this._register(new DisposableStore()); + private _groupListener = this._register(new MutableDisposable()); private _runbuttonToolbar: ToolBar | undefined; private _onDidFocusWidget = this._register(new Emitter()); override get onDidFocus(): Event { return this._onDidFocusWidget.event; } private _onDidChangeSelection = this._register(new Emitter()); readonly onDidChangeSelection = this._onDidChangeSelection.event; + private _onDidChangeScroll = this._register(new Emitter()); + readonly onDidChangeScroll = this._onDidChangeScroll.event; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -137,6 +139,7 @@ export class InteractiveEditor extends EditorPane { ) { super( INTERACTIVE_WINDOW_EDITOR_ID, + group, telemetryService, themeService, storageService @@ -160,7 +163,7 @@ export class InteractiveEditor extends EditorPane { this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(DOM.getWindowById(this.group?.windowId, true).window ?? mainWindow, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); @@ -313,7 +316,7 @@ export class InteractiveEditor extends EditorPane { } private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._notebookWidget.value && input instanceof InteractiveEditorInput) { + if (this._notebookWidget.value && input instanceof InteractiveEditorInput) { if (this._notebookWidget.value.isDisposed) { return; } @@ -328,10 +331,7 @@ export class InteractiveEditor extends EditorPane { } private _loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { - let result: InteractiveEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); if (result) { return result; } @@ -351,7 +351,6 @@ export class InteractiveEditor extends EditorPane { } override async setInput(input: InteractiveEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const group = this.group!; const notebookInput = input.notebookEditorInput; // there currently is a widget which we still own so @@ -362,7 +361,7 @@ export class InteractiveEditor extends EditorPane { this._widgetDisposableStore.clear(); - this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, notebookInput, { + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, notebookInput, { isEmbedded: true, isReadOnly: true, contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ @@ -385,8 +384,9 @@ export class InteractiveEditor extends EditorPane { HoverController.ID, MarkerController.ID ]), - options: this._notebookOptions - }, undefined, this._rootElement ? DOM.getWindow(this._rootElement) : mainWindow); + options: this._notebookOptions, + codeWindow: this.window + }, undefined, this.window); this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { ...{ @@ -532,6 +532,8 @@ export class InteractiveEditor extends EditorPane { } })); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidScroll(() => this._onDidChangeScroll.fire())); + this._syncWithKernel(); } @@ -667,6 +669,17 @@ export class InteractiveEditor extends EditorPane { this._codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); } + getScrollPosition(): IEditorPaneScrollPosition { + return { + scrollTop: this._notebookWidget.value?.scrollTop ?? 0, + scrollLeft: 0 + }; + } + + setScrollPosition(position: IEditorPaneScrollPosition): void { + this._notebookWidget.value?.setScrollTop(position.scrollTop); + } + override focus() { super.focus(); @@ -678,12 +691,9 @@ export class InteractiveEditor extends EditorPane { this._notebookWidget.value!.focus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.value = this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); if (!visible) { this._saveEditorViewState(this.input); diff --git a/code/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts b/code/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts index e8317893d1e..8dd727d49c6 100644 --- a/code/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts +++ b/code/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts @@ -10,13 +10,14 @@ import { isEqual, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorInputCapabilities, GroupIdentifier, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IInteractiveDocumentService } from 'vs/workbench/contrib/interactive/browser/interactiveDocumentService'; import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; -import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IResolvedNotebookEditorModel, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICompositeNotebookEditorInput, NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -44,6 +45,7 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot } private name: string; + private readonly isScratchpad: boolean; get language() { return this._inputModelRef?.object.textEditorModel.getLanguageId() ?? this._initLanguage; @@ -93,10 +95,12 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot @IInteractiveDocumentService interactiveDocumentService: IInteractiveDocumentService, @IInteractiveHistoryService historyService: IInteractiveHistoryService, @INotebookService private readonly _notebookService: INotebookService, - @IFileDialogService private readonly _fileDialogService: IFileDialogService + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + @IConfigurationService configurationService: IConfigurationService ) { const input = NotebookEditorInput.getOrCreate(instantiationService, resource, undefined, 'interactive', {}); super(); + this.isScratchpad = configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; this._notebookEditorInput = input; this._register(this._notebookEditorInput); this.name = title ?? InteractiveEditorInput.windowNames[resource.path] ?? paths.basename(resource.path, paths.extname(resource.path)); @@ -130,10 +134,11 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot } override get capabilities(): EditorInputCapabilities { + const scratchPad = this.isScratchpad ? EditorInputCapabilities.Scratchpad : 0; + return EditorInputCapabilities.Untitled | EditorInputCapabilities.Readonly - | EditorInputCapabilities.AuxWindowUnsupported - | EditorInputCapabilities.Scratchpad; + | scratchPad; } private async _resolveEditorModel() { @@ -221,10 +226,24 @@ export class InteractiveEditorInput extends EditorInput implements ICompositeNot return this.name; } + override isDirty(): boolean { + if (this.isScratchpad) { + return false; + } + + return this._editorModelReference?.isDirty() ?? false; + } + override isModified() { return this._editorModelReference?.isModified() ?? false; } + override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this._editorModelReference && this._editorModelReference.isDirty()) { + await this._editorModelReference.revert(options); + } + } + override dispose() { // we support closing the interactive window without prompt, so the editor model should not be dirty this._editorModelReference?.revert({ soft: true }); diff --git a/code/src/vs/workbench/contrib/issue/browser/issue.contribution.ts b/code/src/vs/workbench/contrib/issue/browser/issue.contribution.ts index 7d19d1cd3c6..28751d1c2c8 100644 --- a/code/src/vs/workbench/contrib/issue/browser/issue.contribution.ts +++ b/code/src/vs/workbench/contrib/issue/browser/issue.contribution.ts @@ -13,10 +13,12 @@ import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common import { WebIssueService } from 'vs/workbench/services/issue/browser/issueService'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + class WebIssueContribution extends BaseIssueContribution { - constructor(@IProductService productService: IProductService) { - super(productService); + constructor(@IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService) { + super(productService, configurationService); } } diff --git a/code/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts b/code/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts new file mode 100644 index 00000000000..ce6c330855b --- /dev/null +++ b/code/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PickerQuickAccessProvider, IPickerQuickAccessItem, FastAndSlowPicks, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Codicon } from 'vs/base/common/codicons'; +import { IssueSource } from 'vs/platform/issue/common/issue'; +import { IProductService } from 'vs/platform/product/common/productService'; + +export class IssueQuickAccess extends PickerQuickAccessProvider { + + static PREFIX = 'issue '; + + constructor( + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ICommandService private readonly commandService: ICommandService, + @IExtensionService private readonly extensionService: IExtensionService, + @IProductService private readonly productService: IProductService + ) { + super(IssueQuickAccess.PREFIX, { canAcceptInBackground: true }); + } + + protected override _getPicks(filter: string): Picks | FastAndSlowPicks | Promise | FastAndSlowPicks> | null { + const issuePicksConst = new Array(); + const issuePicksParts = new Array(); + const extensionIdSet = new Set(); + + // Add default items + const productLabel = this.productService.nameLong; + const marketPlaceLabel = localize("reportExtensionMarketplace", "Extension Marketplace"); + issuePicksConst.push( + { label: productLabel, ariaLabel: productLabel, accept: () => this.commandService.executeCommand('workbench.action.openIssueReporter', { issueSource: IssueSource.VSCode }) }, + { label: marketPlaceLabel, ariaLabel: marketPlaceLabel, accept: () => this.commandService.executeCommand('workbench.action.openIssueReporter', { issueSource: IssueSource.Marketplace }) }, + { type: 'separator', label: localize('extensions', "Extensions") } + ); + + // creates menu from contributed + const menu = this.menuService.createMenu(MenuId.IssueReporter, this.contextKeyService); + + // render menu and dispose + const actions = menu.getActions({ renderShortTitle: true }).flatMap(entry => entry[1]); + + menu.dispose(); + + // create picks from contributed menu + actions.forEach(action => { + if ('source' in action.item && action.item.source) { + extensionIdSet.add(action.item.source.id); + } + + const pick = this._createPick(filter, action); + if (pick) { + issuePicksParts.push(pick); + } + }); + + + // create picks from extensions + this.extensionService.extensions.forEach(extension => { + if (!extension.isBuiltin) { + const pick = this._createPick(filter, undefined, extension); + const id = extension.identifier.value; + if (pick && !extensionIdSet.has(id)) { + issuePicksParts.push(pick); + } + extensionIdSet.add(id); + } + }); + + issuePicksParts.sort((a, b) => { + const aLabel = a.label ?? ''; + const bLabel = b.label ?? ''; + return aLabel.localeCompare(bLabel); + }); + + return [...issuePicksConst, ...issuePicksParts]; + } + + private _createPick(filter: string, action?: MenuItemAction | SubmenuItemAction | undefined, extension?: IRelaxedExtensionDescription): IPickerQuickAccessItem | undefined { + const buttons = [{ + iconClass: ThemeIcon.asClassName(Codicon.info), + tooltip: localize('contributedIssuePage', "Open Extension Page") + }]; + + let label: string; + let trigger: () => TriggerAction; + let accept: () => void; + if (action && 'source' in action.item && action.item.source) { + label = action.item.source?.title; + trigger = () => { + if ('source' in action.item && action.item.source) { + this.commandService.executeCommand('extension.open', action.item.source.id); + } + return TriggerAction.CLOSE_PICKER; + }; + accept = () => { + action.run(); + }; + + } else if (extension) { + label = extension.displayName ?? extension.name; + trigger = () => { + this.commandService.executeCommand('extension.open', extension.identifier.value); + return TriggerAction.CLOSE_PICKER; + }; + accept = () => { + this.commandService.executeCommand('workbench.action.openIssueReporter', extension.identifier.value); + }; + + } else { + return undefined; + } + + return { + label, + highlights: { label: matchesFuzzy(filter, label, true) ?? undefined }, + buttons, + trigger, + accept + }; + } +} diff --git a/code/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/code/src/vs/workbench/contrib/issue/common/issue.contribution.ts index 05518d0f4aa..9e6ee979ac9 100644 --- a/code/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/code/src/vs/workbench/contrib/issue/common/issue.contribution.ts @@ -12,6 +12,8 @@ import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Disposable } from 'vs/base/common/lifecycle'; const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; const OpenIssueReporterApiId = 'vscode.openIssueReporter'; @@ -57,15 +59,18 @@ interface OpenIssueReporterArgs { readonly extensionData?: string; } -export class BaseIssueContribution implements IWorkbenchContribution { +export class BaseIssueContribution extends Disposable implements IWorkbenchContribution { constructor( - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, ) { + super(); + if (!productService.reportIssueUrl) { return; } - CommandsRegistry.registerCommand({ + this._register(CommandsRegistry.registerCommand({ id: OpenIssueReporterActionId, handler: function (accessor, args?: string | [string] | OpenIssueReporterArgs) { const data: Partial = @@ -78,9 +83,9 @@ export class BaseIssueContribution implements IWorkbenchContribution { return accessor.get(IWorkbenchIssueService).openReporter(data); }, metadata: OpenIssueReporterCommandMetadata - }); + })); - CommandsRegistry.registerCommand({ + this._register(CommandsRegistry.registerCommand({ id: OpenIssueReporterApiId, handler: function (accessor, args?: string | [string] | OpenIssueReporterArgs) { const data: Partial = @@ -93,7 +98,7 @@ export class BaseIssueContribution implements IWorkbenchContribution { return accessor.get(IWorkbenchIssueService).openReporter(data); }, metadata: OpenIssueReporterCommandMetadata - }); + })); const reportIssue: ICommandAction = { id: OpenIssueReporterActionId, @@ -101,15 +106,15 @@ export class BaseIssueContribution implements IWorkbenchContribution { category: Categories.Help }; - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: reportIssue }); + this._register(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: reportIssue })); - MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + this._register(MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { group: '3_feedback', command: { id: OpenIssueReporterActionId, title: localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue") }, order: 3 - }); + })); } } diff --git a/code/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/code/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 05bc0632624..d6c76d7fbd9 100644 --- a/code/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/code/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -19,18 +19,52 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INativeHostService } from 'vs/platform/native/common/native'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IIssueMainService, IssueType } from 'vs/platform/issue/common/issue'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; +import { IssueQuickAccess } from 'vs/workbench/contrib/issue/browser/issueQuickAccess'; + //#region Issue Contribution class NativeIssueContribution extends BaseIssueContribution { constructor( - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService ) { - super(productService); + super(productService, configurationService); if (productService.reportIssueUrl) { - registerAction2(ReportPerformanceIssueUsingReporterAction); + this._register(registerAction2(ReportPerformanceIssueUsingReporterAction)); + } + + let disposable: IDisposable | undefined; + + const registerQuickAccessProvider = () => { + disposable = Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: IssueQuickAccess, + prefix: IssueQuickAccess.PREFIX, + contextKey: 'inReportIssuePicker', + placeholder: localize('tasksQuickAccessPlaceholder', "Type the name of an extension to report on."), + helpEntries: [{ + description: localize('openIssueReporter', "Open Issue Reporter"), + commandId: 'workbench.action.openIssueReporter' + }] + }); + }; + + this._register(configurationService.onDidChangeConfiguration(e => { + if (!configurationService.getValue('extensions.experimental.issueQuickAccess') && disposable) { + disposable.dispose(); + disposable = undefined; + } else if (!disposable) { + registerQuickAccessProvider(); + } + })); + + if (configurationService.getValue('extensions.experimental.issueQuickAccess')) { + registerQuickAccessProvider(); } } } @@ -133,5 +167,4 @@ registerAction2(StopTracing); CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { return accessor.get(IIssueMainService).getSystemStatus(); }); - //#endregion diff --git a/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index f7fb23f59c6..4354ad022df 100644 --- a/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -55,7 +55,6 @@ .monaco-workbench .hover-language-status { display: flex; - padding: 4px 8px; } .monaco-workbench .hover-language-status:not(:last-child) { diff --git a/code/src/vs/workbench/contrib/logs/browser/logs.contribution.ts b/code/src/vs/workbench/contrib/logs/browser/logs.contribution.ts index 23f019bac51..15bd494c20a 100644 --- a/code/src/vs/workbench/contrib/logs/browser/logs.contribution.ts +++ b/code/src/vs/workbench/contrib/logs/browser/logs.contribution.ts @@ -25,7 +25,7 @@ class WebLogOutputChannels extends Disposable implements IWorkbenchContribution private registerWebContributions(): void { this.instantiationService.createInstance(LogsDataCleaner); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: OpenWindowSessionLogFileAction.ID, @@ -37,7 +37,7 @@ class WebLogOutputChannels extends Disposable implements IWorkbenchContribution run(servicesAccessor: ServicesAccessor): Promise { return servicesAccessor.get(IInstantiationService).createInstance(OpenWindowSessionLogFileAction, OpenWindowSessionLogFileAction.ID, OpenWindowSessionLogFileAction.TITLE.value).run(); } - }); + })); } diff --git a/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts b/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts index 9feccec6965..522b64a8cd6 100644 --- a/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts +++ b/code/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts @@ -12,6 +12,8 @@ import { isString, isUndefined } from 'vs/base/common/types'; import { EXTENSION_IDENTIFIER_WITH_LOG_REGEX } from 'vs/platform/environment/common/environmentService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { parse } from 'vs/base/common/json'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; interface ParsedArgvLogLevels { default?: LogLevel; @@ -26,17 +28,27 @@ export interface IDefaultLogLevelsService { readonly _serviceBrand: undefined; + /** + * An event which fires when default log levels are changed + */ + readonly onDidChangeDefaultLogLevels: Event; + getDefaultLogLevels(): Promise; + getDefaultLogLevel(extensionId?: string): Promise; + setDefaultLogLevel(logLevel: LogLevel, extensionId?: string): Promise; migrateLogLevels(): void; } -class DefaultLogLevelsService implements IDefaultLogLevelsService { +class DefaultLogLevelsService extends Disposable implements IDefaultLogLevelsService { _serviceBrand: undefined; + private _onDidChangeDefaultLogLevels = this._register(new Emitter); + readonly onDidChangeDefaultLogLevels = this._onDidChangeDefaultLogLevels.event; + constructor( @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService, @@ -44,6 +56,7 @@ class DefaultLogLevelsService implements IDefaultLogLevelsService { @ILogService private readonly logService: ILogService, @ILoggerService private readonly loggerService: ILoggerService, ) { + super(); } async getDefaultLogLevels(): Promise { @@ -54,11 +67,20 @@ class DefaultLogLevelsService implements IDefaultLogLevelsService { }; } + async getDefaultLogLevel(extensionId?: string): Promise { + const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; + if (extensionId) { + extensionId = extensionId.toLowerCase(); + return this._getDefaultLogLevel(argvLogLevel, extensionId); + } else { + return this._getDefaultLogLevel(argvLogLevel); + } + } + async setDefaultLogLevel(defaultLogLevel: LogLevel, extensionId?: string): Promise { const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; if (extensionId) { extensionId = extensionId.toLowerCase(); - const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; const currentDefaultLogLevel = this._getDefaultLogLevel(argvLogLevel, extensionId); argvLogLevel.extensions = argvLogLevel.extensions ?? []; const extension = argvLogLevel.extensions.find(([extension]) => extension === extensionId); @@ -82,6 +104,7 @@ class DefaultLogLevelsService implements IDefaultLogLevelsService { this.loggerService.setLogLevel(defaultLogLevel); } } + this._onDidChangeDefaultLogLevels.fire(); } private _getDefaultLogLevel(argvLogLevels: ParsedArgvLogLevels, extension?: string): LogLevel { diff --git a/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 19257411641..b96ba30f68c 100644 --- a/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/code/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -195,7 +195,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } private registerShowWindowLogAction(): void { - registerAction2(class ShowWindowLogAction extends Action2 { + this._register(registerAction2(class ShowWindowLogAction extends Action2 { constructor() { super({ id: showWindowLogActionId, @@ -208,9 +208,8 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { const outputService = servicesAccessor.get(IOutputService); outputService.showChannel(windowLogId); } - }); + })); } - } class LogLevelMigration implements IWorkbenchContribution { diff --git a/code/src/vs/workbench/contrib/logs/common/logsActions.ts b/code/src/vs/workbench/contrib/logs/common/logsActions.ts index 519fab7f93a..3ececbb1223 100644 --- a/code/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/code/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -5,14 +5,14 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; -import { ILoggerService, LogLevel, isLogLevel } from 'vs/platform/log/common/log'; +import { ILoggerService, LogLevel, LogLevelToLocalizedString, isLogLevel } from 'vs/platform/log/common/log'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { dirname, basename, isEqual } from 'vs/base/common/resources'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IOutputService } from 'vs/workbench/services/output/common/output'; +import { IOutputChannelDescriptor, IOutputService } from 'vs/workbench/services/output/common/output'; import { extensionTelemetryLogChannelId, telemetryLogId } from 'vs/platform/telemetry/common/telemetryUtils'; import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels'; import { Codicon } from 'vs/base/common/codicons'; @@ -52,7 +52,7 @@ export class SetLogLevelAction extends Action { const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); for (const channel of this.outputService.getChannelDescriptors()) { - if (!channel.log || !channel.file || channel.id === telemetryLogId || channel.id === extensionTelemetryLogChannelId) { + if (!SetLogLevelAction.isLevelSettable(channel) || !channel.file) { continue; } const channelLogLevel = this.loggerService.getLogLevel(channel.file) ?? logLevel; @@ -96,6 +96,10 @@ export class SetLogLevelAction extends Action { }); } + static isLevelSettable(channel: IOutputChannelDescriptor): boolean { + return channel.log && channel.file !== undefined && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; + } + private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise { const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default; @@ -141,15 +145,7 @@ export class SetLogLevelAction extends Action { } private getLabel(level: LogLevel, current?: LogLevel): string { - let label: string; - switch (level) { - case LogLevel.Trace: label = nls.localize('trace', "Trace"); break; - case LogLevel.Debug: label = nls.localize('debug', "Debug"); break; - case LogLevel.Info: label = nls.localize('info', "Info"); break; - case LogLevel.Warning: label = nls.localize('warn', "Warning"); break; - case LogLevel.Error: label = nls.localize('err', "Error"); break; - case LogLevel.Off: label = nls.localize('off', "Off"); break; - } + const label = LogLevelToLocalizedString(level).value; return level === current ? `$(check) ${label}` : label; } diff --git a/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 8ac086cada8..9b18b5cd028 100644 --- a/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -205,7 +205,7 @@ export async function renderMarkdownDocument( } if (typeof lang !== 'string') { - callback(null, `${escape(code)}`); + callback(null, escape(code)); return ''; } @@ -217,7 +217,7 @@ export async function renderMarkdownDocument( const languageId = languageService.getLanguageIdByLanguageName(lang) ?? languageService.getLanguageIdByLanguageName(lang.split(/\s+|:|,|(?!^)\{|\?]/, 1)[0]); const html = await tokenizeToString(languageService, code, languageId); - callback(null, `${html}`); + callback(null, html); }); return ''; }; diff --git a/code/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/code/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index c4cf0aa94fe..095a2978564 100644 --- a/code/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/code/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts @@ -17,7 +17,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; const codeSettingRegex = /^/; -const codeFeatureRegex = /^/; export class SimpleSettingRenderer { private _defaultSettings: DefaultSettings; @@ -45,10 +44,10 @@ export class SimpleSettingRenderer { getHtmlRenderer(): (html: string) => string { return (html): string => { - const match = codeSettingRegex.exec(html) ?? codeFeatureRegex.exec(html); + const match = codeSettingRegex.exec(html); if (match && match.length === 4) { const settingId = match[2]; - const rendered = this.render(settingId, match[3], match[1] === 'codefeature'); + const rendered = this.render(settingId, match[3]); if (rendered) { html = html.replace(codeSettingRegex, rendered); } @@ -61,10 +60,6 @@ export class SimpleSettingRenderer { return `${Schemas.codeSetting}://${settingId}${value ? `/${value}` : ''}`; } - featureToUriString(settingId: string, value?: any): string { - return `${Schemas.codeFeature}://${settingId}${value ? `/${value}` : ''}`; - } - private settingsGroups: ISettingsGroup[] | undefined = undefined; private getSetting(settingId: string): ISetting | undefined { if (!this.settingsGroups) { @@ -106,16 +101,13 @@ export class SimpleSettingRenderer { } } - private render(settingId: string, newValue: string, asFeature: boolean): string | undefined { + private render(settingId: string, newValue: string): string | undefined { const setting = this.getSetting(settingId); if (!setting) { return ''; } - if (asFeature) { - return this.renderFeature(setting, newValue); - } else { - return this.renderSetting(setting, newValue); - } + + return this.renderSetting(setting, newValue); } private viewInSettingsMessage(settingId: string, alreadyDisplayed: boolean) { @@ -176,15 +168,6 @@ export class SimpleSettingRenderer { `; } - private renderFeature(setting: ISetting, newValue: string): string | undefined { - const href = this.featureToUriString(setting.key, newValue); - const parsedValue = this.parseValue(setting.key, newValue); - const isChecked = this._configurationService.getValue(setting.key) === parsedValue; - this._featuredSettings.set(setting.key, parsedValue); - const title = nls.localize('changeFeatureTitle', "Toggle feature with setting {0}", setting.key); - return `
`; - } - private getSettingMessage(setting: ISetting, newValue: boolean | string | number): string | undefined { if (setting.type === 'boolean') { return this.booleanSettingMessage(setting, newValue as boolean); @@ -289,21 +272,6 @@ export class SimpleSettingRenderer { }); } - private async setFeatureState(uri: URI) { - const settingId = uri.authority; - const newSettingValue = this.parseValue(uri.authority, uri.path.substring(1)); - let valueToSetSetting: any; - if (this._updatedSettings.has(settingId)) { - valueToSetSetting = this._updatedSettings.get(settingId); - this._updatedSettings.delete(settingId); - } else if (newSettingValue !== this._configurationService.getValue(settingId)) { - valueToSetSetting = newSettingValue; - } else { - valueToSetSetting = undefined; - } - await this._configurationService.updateValue(settingId, valueToSetSetting, ConfigurationTarget.USER); - } - async updateSetting(uri: URI, x: number, y: number) { if (uri.scheme === Schemas.codeSetting) { type ReleaseNotesSettingUsedClassification = { @@ -318,8 +286,6 @@ export class SimpleSettingRenderer { settingId: uri.authority }); return this.showContextMenu(uri, x, y); - } else if (uri.scheme === Schemas.codeFeature) { - return this.setFeatureState(uri); } } } diff --git a/code/src/vs/workbench/contrib/markers/browser/markersTable.ts b/code/src/vs/workbench/contrib/markers/browser/markersTable.ts index bb63d92e9d2..44467e7e01d 100644 --- a/code/src/vs/workbench/contrib/markers/browser/markersTable.ts +++ b/code/src/vs/workbench/contrib/markers/browser/markersTable.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { ITableContextMenuEvent, ITableEvent, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IOpenEvent, IWorkbenchTableOptions, WorkbenchTable } from 'vs/platform/list/browser/listService'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; @@ -43,6 +43,7 @@ interface IMarkerCodeColumnTemplateData { readonly sourceLabel: HighlightedLabel; readonly codeLabel: HighlightedLabel; readonly codeLink: Link; + readonly templateDisposable: DisposableStore; } interface IMarkerFileColumnTemplateData { @@ -121,17 +122,18 @@ class MarkerCodeColumnRenderer implements ITableRenderer { @@ -185,7 +189,9 @@ class MarkerMessageColumnRenderer implements ITableRenderer { @@ -216,7 +222,10 @@ class MarkerFileColumnRenderer implements ITableRenderer { @@ -236,7 +245,9 @@ class MarkerOwnerColumnRenderer implements ITableRenderer { diff --git a/code/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/code/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 180a378ac81..a3556f80678 100644 --- a/code/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/code/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -51,6 +51,8 @@ import { MarkersContextKeys, MarkersViewMode } from 'vs/workbench/contrib/marker import { unsupportedSchemas } from 'vs/platform/markers/common/markerService'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import Severity from 'vs/base/common/severity'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; interface IResourceMarkersTemplateData { readonly resourceLabel: IResourceLabel; @@ -285,6 +287,7 @@ class MarkerWidget extends Disposable { private readonly icon: HTMLElement; private readonly iconContainer: HTMLElement; private readonly messageAndDetailsContainer: HTMLElement; + private readonly messageAndDetailsContainerHover: ICustomHover; private readonly disposables = this._register(new DisposableStore()); constructor( @@ -304,6 +307,7 @@ class MarkerWidget extends Disposable { this.iconContainer = dom.append(parent, dom.$('')); this.icon = dom.append(this.iconContainer, dom.$('')); this.messageAndDetailsContainer = dom.append(parent, dom.$('.marker-message-details-container')); + this.messageAndDetailsContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.messageAndDetailsContainer, '')); } render(element: Marker, filterData: MarkerFilterData | undefined): void { @@ -366,13 +370,13 @@ class MarkerWidget extends Disposable { const viewState = this.markersViewModel.getViewModel(element); const multiline = !viewState || viewState.multiline; const lineMatches = filterData && filterData.lineMatches || []; - this.messageAndDetailsContainer.title = element.marker.message; + this.messageAndDetailsContainerHover.update(element.marker.message); const lineElements: HTMLElement[] = []; for (let index = 0; index < (multiline ? lines.length : 1); index++) { const lineElement = dom.append(this.messageAndDetailsContainer, dom.$('.marker-message-line')); const messageElement = dom.append(lineElement, dom.$('.marker-message')); - const highlightedLabel = new HighlightedLabel(messageElement); + const highlightedLabel = this.disposables.add(new HighlightedLabel(messageElement)); highlightedLabel.set(lines[index].length > 1000 ? `${lines[index].substring(0, 1000)}...` : lines[index], lineMatches[index]); if (lines[index] === '') { lineElement.style.height = `${VirtualDelegate.LINE_HEIGHT}px`; @@ -387,18 +391,18 @@ class MarkerWidget extends Disposable { parent.classList.add('details-container'); if (marker.source || marker.code) { - const source = new HighlightedLabel(dom.append(parent, dom.$('.marker-source'))); + const source = this.disposables.add(new HighlightedLabel(dom.append(parent, dom.$('.marker-source')))); const sourceMatches = filterData && filterData.sourceMatches || []; source.set(marker.source, sourceMatches); if (marker.code) { if (typeof marker.code === 'string') { - const code = new HighlightedLabel(dom.append(parent, dom.$('.marker-code'))); + const code = this.disposables.add(new HighlightedLabel(dom.append(parent, dom.$('.marker-code')))); const codeMatches = filterData && filterData.codeMatches || []; code.set(marker.code, codeMatches); } else { const container = dom.$('.marker-code'); - const code = new HighlightedLabel(container); + const code = this.disposables.add(new HighlightedLabel(container)); const link = marker.code.target.toString(true); this.disposables.add(new Link(parent, { href: link, label: container, title: link }, undefined, this._openerService)); const codeMatches = filterData && filterData.codeMatches || []; @@ -443,15 +447,15 @@ export class RelatedInformationRenderer implements ITreeRenderer range.endLineNumber) { @@ -20,23 +20,23 @@ export function rangeContainsPosition(range: Range, position: Position): boolean return true; } -export function lengthOfRange(range: Range): LengthObj { +export function lengthOfRange(range: Range): TextLength { if (range.startLineNumber === range.endLineNumber) { - return new LengthObj(0, range.endColumn - range.startColumn); + return new TextLength(0, range.endColumn - range.startColumn); } else { - return new LengthObj(range.endLineNumber - range.startLineNumber, range.endColumn - 1); + return new TextLength(range.endLineNumber - range.startLineNumber, range.endColumn - 1); } } -export function lengthBetweenPositions(position1: Position, position2: Position): LengthObj { +export function lengthBetweenPositions(position1: Position, position2: Position): TextLength { if (position1.lineNumber === position2.lineNumber) { - return new LengthObj(0, position2.column - position1.column); + return new TextLength(0, position2.column - position1.column); } else { - return new LengthObj(position2.lineNumber - position1.lineNumber, position2.column - 1); + return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1); } } -export function addLength(position: Position, length: LengthObj): Position { +export function addLength(position: Position, length: TextLength): Position { if (length.lineCount === 0) { return new Position(position.lineNumber, position.column + length.columnCount); } else { diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts index f6ccc0cf7a3..c6d9664b4be 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts @@ -8,7 +8,7 @@ import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { isDefined } from 'vs/base/common/types'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; import { ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; import { addLength, lengthBetweenPositions, lengthOfRange } from 'vs/workbench/contrib/mergeEditor/browser/model/rangeUtils'; @@ -49,7 +49,7 @@ export function getAlignments(m: ModifiedBaseRange): LineAlignment[] { if (shouldAdd) { result.push(lineAlignment); } else { - if (m.length.isGreaterThan(new LengthObj(1, 0))) { + if (m.length.isGreaterThan(new TextLength(1, 0))) { result.push([ m.output1Pos ? m.output1Pos.lineNumber + 1 : undefined, m.inputPos.lineNumber + 1, @@ -75,7 +75,7 @@ interface CommonRangeMapping { output1Pos: Position | undefined; output2Pos: Position | undefined; inputPos: Position; - length: LengthObj; + length: TextLength; } function toEqualRangeMappings(diffs: RangeMapping[], inputRange: Range, outputRange: Range): RangeMapping[] { diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 45af28b94f0..3609b0046fa 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -108,6 +108,7 @@ export class MergeEditor extends AbstractTextEditor { private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); constructor( + group: IEditorGroup, @IInstantiationService instantiation: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @@ -121,7 +122,7 @@ export class MergeEditor extends AbstractTextEditor { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(MergeEditor.ID, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override dispose(): void { @@ -354,7 +355,7 @@ export class MergeEditor extends AbstractTextEditor { // all empty -> replace this editor with a normal editor for result that.editorService.replaceEditors( [{ editor: input, replacement: { resource: input.result, options: { preserveFocus: true } }, forceReplaceDirty: true }], - that.group ?? that.editorGroupService.activeGroup + that.group ); } }); @@ -467,8 +468,8 @@ export class MergeEditor extends AbstractTextEditor { return super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { if (visible) { diff --git a/code/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts b/code/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts index ae714c10766..85c475e3c2f 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { DocumentRangeMap, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; suite('merge editor mapping', () => { @@ -53,19 +53,19 @@ function parsePos(str: string): Position { return new Position(parseInt(lineCount, 10), parseInt(columnCount, 10)); } -function parseLengthObj(str: string): LengthObj { +function parseLengthObj(str: string): TextLength { const [lineCount, columnCount] = str.split(':'); - return new LengthObj(parseInt(lineCount, 10), parseInt(columnCount, 10)); + return new TextLength(parseInt(lineCount, 10), parseInt(columnCount, 10)); } -function toPosition(length: LengthObj): Position { +function toPosition(length: TextLength): Position { return new Position(length.lineCount + 1, length.columnCount + 1); } function createDocumentRangeMap(items: ([string, string] | string)[]) { const mappings: RangeMapping[] = []; - let lastLen1 = new LengthObj(0, 0); - let lastLen2 = new LengthObj(0, 0); + let lastLen1 = new TextLength(0, 0); + let lastLen2 = new TextLength(0, 0); for (const item of items) { if (typeof item === 'string') { const len = parseLengthObj(item); diff --git a/code/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/code/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index 6f66dbec274..ffe7a8738f5 100644 --- a/code/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/code/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -19,7 +19,7 @@ import { ICompositeControl } from 'vs/workbench/common/composite'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { URI } from 'vs/base/common/uri'; import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; @@ -39,6 +39,7 @@ export class MultiDiffEditor extends AbstractEditorWithViewState { + createMultiDiffEditorInput: (multiDiffEditor: IResourceMultiDiffEditorInput): EditorInputWithOptions => { return { - editor: MultiDiffEditorInput.fromResourceMultiDiffEditorInput(diffListEditor, instantiationService), + editor: MultiDiffEditorInput.fromResourceMultiDiffEditorInput(multiDiffEditor, instantiationService), }; }, } diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 73f0201827b..c945e31d356 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -7,7 +7,7 @@ import { localize, localize2 } from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { cellRangeToViewCells, expandCellRangesWithHiddenCells, getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -16,7 +16,7 @@ import { CellEditType, ICellEditOperation, ISelectionState, SelectionStateType } import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import * as platform from 'vs/base/common/platform'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { CellOverflowToolbarGroups, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { CellOverflowToolbarGroups, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, NOTEBOOK_OUTPUT_WEBVIEW_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; @@ -41,7 +41,7 @@ function _log(loggerService: ILogService, str: string) { } } -function getFocusedWebviewDelegate(accessor: ServicesAccessor): IWebview | undefined { +function getFocusedEditor(accessor: ServicesAccessor) { const loggerService = accessor.get(ILogService); const editorService = accessor.get(IEditorService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); @@ -59,9 +59,21 @@ function getFocusedWebviewDelegate(accessor: ServicesAccessor): IWebview | undef _log(loggerService, '[Revive Webview] Notebook editor backlayer webview is not focused, bypass'); return; } + // If none of the outputs have focus, then webview is not focused + const view = editor.getViewModel(); + if (view && view.viewCells.every(cell => !cell.outputIsFocused && !cell.outputIsHovered)) { + return; + } - const webview = editor.getInnerWebview(); - _log(loggerService, '[Revive Webview] Notebook editor backlayer webview is focused'); + return { editor, loggerService }; +} +function getFocusedWebviewDelegate(accessor: ServicesAccessor): IWebview | undefined { + const result = getFocusedEditor(accessor); + if (!result) { + return; + } + const webview = result.editor.getInnerWebview(); + _log(result.loggerService, '[Revive Webview] Notebook editor backlayer webview is focused'); return webview; } @@ -74,6 +86,11 @@ function withWebview(accessor: ServicesAccessor, f: (webviewe: IWebview) => void return false; } +function withEditor(accessor: ServicesAccessor, f: (editor: INotebookEditor) => boolean) { + const result = getFocusedEditor(accessor); + return result ? f(result.editor) : false; +} + const PRIORITY = 105; UndoCommand.addImplementation(PRIORITY, 'notebook-webview', accessor => { @@ -96,7 +113,6 @@ CutAction?.addImplementation(PRIORITY, 'notebook-webview', accessor => { return withWebview(accessor, webview => webview.cut()); }); - export function runPasteCells(editor: INotebookEditor, activeCell: ICellViewModel | undefined, pasteCells: { items: NotebookCellTextModel[]; isCopy: boolean; @@ -422,6 +438,7 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, when: NOTEBOOK_EDITOR_FOCUSED, group: CellOverflowToolbarGroups.Copy, + order: 2, }, keybinding: platform.isNative ? undefined : { primary: KeyMod.CtrlCmd | KeyCode.KeyC, @@ -447,6 +464,7 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), group: CellOverflowToolbarGroups.Copy, + order: 1, }, keybinding: platform.isNative ? undefined : { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -472,6 +490,7 @@ registerAction2(class extends NotebookAction { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE), group: CellOverflowToolbarGroups.Copy, + order: 3, }, keybinding: platform.isNative ? undefined : { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -568,3 +587,37 @@ registerAction2(class extends Action2 { } } }); + + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.output.selectAll', + title: localize('notebook.cell.output.selectAll', "Select All"), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyA, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_OUTPUT_FOCUSED), + weight: NOTEBOOK_OUTPUT_WEBVIEW_ACTION_WEIGHT + } + }); + } + + async runWithContext(accessor: ServicesAccessor, _context: INotebookCellActionContext) { + withEditor(accessor, editor => { + if (!editor.hasEditorFocus()) { + return false; + } + if (editor.hasEditorFocus() && !editor.hasWebviewFocus()) { + return true; + } + const cell = editor.getActiveCell(); + if (!cell || !cell.outputIsFocused || !editor.hasWebviewFocus()) { + return true; + } + editor.selectOutputContent(cell); + return true; + }); + + } +}); diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index fd9a432d6c3..0c4b949b65e 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -186,6 +186,14 @@ export class NotebookFindInputFilterButton extends Disposable { return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */; } + enable(): void { + this.container.setAttribute('aria-disabled', String(false)); + } + + disable(): void { + this.container.setAttribute('aria-disabled', String(true)); + } + applyStyles(filterChecked: boolean): void { const toggleStyles = this._toggleStyles; diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index 52eea945ec6..cb9e490f91d 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -358,6 +358,7 @@ registerAction2(class extends NotebookCellAction { NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), EditorContextKeys.editorTextFocus, + NOTEBOOK_OUTPUT_FOCUSED.negate(), // Webview handles Shift+PageUp for selection of output contents ), primary: KeyMod.Shift | KeyCode.PageUp, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT @@ -406,6 +407,7 @@ registerAction2(class extends NotebookCellAction { NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), EditorContextKeys.editorTextFocus, + NOTEBOOK_OUTPUT_FOCUSED.negate(), // Webview handles Shift+PageDown for selection of output contents ), primary: KeyMod.Shift | KeyCode.PageDown, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts index b9cdadad7ff..18172e308d1 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts @@ -82,13 +82,16 @@ export class NotebookVariableDataSource implements IAsyncDataSource variablePageSize) { - // TODO: improve handling of large number of children - const indexedChildCountLimit = 100000; - const limit = Math.min(parent.indexedChildrenCount, indexedChildCountLimit); - for (let start = 0; start < limit; start += variablePageSize) { - let end = start + variablePageSize; - if (end > limit) { - end = limit; + + const nestedPageSize = Math.floor(Math.max(parent.indexedChildrenCount / variablePageSize, 100)); + + const indexedChildCountLimit = 1_000_000; + let start = parent.indexStart ?? 0; + const last = start + Math.min(parent.indexedChildrenCount, indexedChildCountLimit); + for (; start < last; start += nestedPageSize) { + let end = start + nestedPageSize; + if (end > last) { + end = last; } childNodes.push({ @@ -108,7 +111,7 @@ export class NotebookVariableDataSource implements IAsyncDataSource, _index: number, template: NotebookOutlineTemplate, _height: number | undefined): void { @@ -79,7 +105,8 @@ class NotebookOutlineRenderer implements ITreeRenderer 0); + NotebookOutlineContext.CellHasHeader.bindTo(scopedContextKeyService).set(node.element.level !== 7); + NotebookOutlineContext.OutlineElementTarget.bindTo(scopedContextKeyService).set(this._target); + this.setupFolding(isCodeCell, nbViewModel, scopedContextKeyService, template, nbCell); + + const outlineEntryToolbar = template.elementDisposables.add(new ToolBar(template.actionMenu, this._contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + return this._instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); + } + return undefined; + }, + })); + + const menu = template.elementDisposables.add(this._menuService.createMenu(MenuId.NotebookOutlineActionMenu, scopedContextKeyService)); + const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: node.element }); + outlineEntryToolbar.setActions(actions.primary, actions.secondary); + + this.setupToolbarListeners(outlineEntryToolbar, menu, actions, node.element, template); + } } disposeTemplate(templateData: NotebookOutlineTemplate): void { templateData.iconLabel.dispose(); + templateData.elementDisposables.clear(); + } + + disposeElement(element: ITreeNode, index: number, templateData: NotebookOutlineTemplate, height: number | undefined): void { + templateData.elementDisposables.clear(); + DOM.clearNode(templateData.actionMenu); + } + + private setupFolding(isCodeCell: boolean, nbViewModel: INotebookViewModel, scopedContextKeyService: IContextKeyService, template: NotebookOutlineTemplate, nbCell: ICellViewModel) { + const foldingState = isCodeCell ? CellFoldingState.None : ((nbCell as MarkupCellViewModel).foldingState); + const foldingStateCtx = NotebookOutlineContext.CellFoldingState.bindTo(scopedContextKeyService); + foldingStateCtx.set(foldingState); + + if (!isCodeCell) { + template.elementDisposables.add(nbViewModel.onDidFoldingStateChanged(() => { + const foldingState = (nbCell as MarkupCellViewModel).foldingState; + NotebookOutlineContext.CellFoldingState.bindTo(scopedContextKeyService).set(foldingState); + foldingStateCtx.set(foldingState); + })); + } + } + + private setupToolbarListeners(toolbar: ToolBar, menu: IMenu, initActions: { primary: IAction[]; secondary: IAction[] }, entry: OutlineEntry, templateData: NotebookOutlineTemplate): void { + // same fix as in cellToolbars setupListeners re #103926 + let dropdownIsVisible = false; + let deferredUpdate: (() => void) | undefined; + + toolbar.setActions(initActions.primary, initActions.secondary); + templateData.elementDisposables.add(menu.onDidChange(() => { + if (dropdownIsVisible) { + const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: entry }); + deferredUpdate = () => toolbar.setActions(actions.primary, actions.secondary); + + return; + } + + const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: entry }); + toolbar.setActions(actions.primary, actions.secondary); + })); + + templateData.container.classList.remove('notebook-outline-toolbar-dropdown-active'); + templateData.elementDisposables.add(toolbar.onDidChangeDropdownVisibility(visible => { + dropdownIsVisible = visible; + if (visible) { + templateData.container.classList.add('notebook-outline-toolbar-dropdown-active'); + } else { + templateData.container.classList.remove('notebook-outline-toolbar-dropdown-active'); + } + + if (deferredUpdate && !visible) { + disposableTimeout(() => { + deferredUpdate?.(); + }, 0, templateData.elementDisposables); + + deferredUpdate = undefined; + } + })); + } } +function getOutlineToolbarActions(menu: IMenu, args?: NotebookSectionArgs): { primary: IAction[]; secondary: IAction[] } { + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + // TODO: @Yoyokrazy bring the "inline" back when there's an appropriate run in section icon + createAndFillInActionBarActions(menu, { shouldForwardArgs: true, arg: args }, result); //, g => /^inline/.test(g)); + + return result; +} + class NotebookOutlineAccessibility implements IListAccessibilityProvider { getAriaLabel(element: OutlineEntry): string | null { return element.label; @@ -183,7 +311,7 @@ class NotebookQuickPickProvider implements IQuickPickDataSource { class NotebookComparator implements IOutlineComparator { - private readonly _collator = new WindowIdleValue(mainWindow, () => new Intl.Collator(undefined, { numeric: true })); + private readonly _collator = new DOM.WindowIdleValue(mainWindow, () => new Intl.Collator(undefined, { numeric: true })); compareByPosition(a: OutlineEntry, b: OutlineEntry): number { return a.index - b.index; @@ -251,7 +379,7 @@ export class NotebookCellOutline implements IOutline { installSelectionListener(); const treeDataSource: IDataSource = { getChildren: parent => parent instanceof NotebookCellOutline ? (this._outlineProvider?.entries ?? []) : parent.children }; const delegate = new NotebookOutlineVirtualDelegate(); - const renderers = [instantiationService.createInstance(NotebookOutlineRenderer)]; + const renderers = [instantiationService.createInstance(NotebookOutlineRenderer, this._editor.getControl(), _target)]; const comparator = new NotebookComparator(); const options: IWorkbenchDataTreeOptions = { @@ -404,8 +532,15 @@ export class NotebookOutlineCreator implements IOutlineCreator(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookOutlineCreator, LifecyclePhase.Eventually); +export const NotebookOutlineContext = { + CellKind: new RawContextKey('notebookCellKind', undefined), + CellHasChildren: new RawContextKey('notebookCellHasChildren', false), + CellHasHeader: new RawContextKey('notebookCellHasHeader', false), + CellFoldingState: new RawContextKey('notebookCellFoldingState', CellFoldingState.None), + OutlineElementTarget: new RawContextKey('notebookOutlineElementTarget', undefined), +}; +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookOutlineCreator, LifecyclePhase.Eventually); Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'notebook', @@ -417,6 +552,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis default: false, markdownDescription: localize('outline.showCodeCells', "When enabled notebook outline shows code cells.") }, + 'notebook.outline.showNonHeaderMarkdownCells': { + type: 'boolean', + default: false, + markdownDescription: localize('outline.showNonHeaderMarkdownCells', "When enabled, notebook outline will show non-header markdown cell entries. When disabled, only markdown cells containing a header are shown.") + }, 'notebook.breadcrumbs.showCodeCells': { type: 'boolean', default: true, @@ -429,3 +569,58 @@ Registry.as(ConfigurationExtensions.Configuration).regis }, } }); + +MenuRegistry.appendMenuItem(MenuId.ViewTitle, { + submenu: MenuId.NotebookOutlineFilter, + title: localize('filter', "Filter Entries"), + icon: Codicon.filter, + group: 'navigation', + order: -1, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', IOutlinePane.Id), NOTEBOOK_IS_ACTIVE_EDITOR), +}); + +registerAction2(class ToggleCodeCellEntries extends Action2 { + constructor() { + super({ + id: 'notebook.outline.toggleCodeCells', + title: localize('toggleCodeCells', "Toggle Code Cells"), + f1: false, + toggled: { + condition: ContextKeyExpr.equals('config.notebook.outline.showCodeCells', true) + }, + menu: { + id: MenuId.NotebookOutlineFilter, + group: 'filter', + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const configurationService = accessor.get(IConfigurationService); + const showCodeCells = configurationService.getValue('notebook.outline.showCodeCells'); + configurationService.updateValue('notebook.outline.showCodeCells', !showCodeCells); + } +}); + +registerAction2(class ToggleNonHeaderMarkdownCells extends Action2 { + constructor() { + super({ + id: 'notebook.outline.toggleNonHeaderMarkdownCells', + title: localize('toggleNonHeaderMarkdownCells', "Toggle Non-Header Markdown Cells"), + f1: false, + toggled: { + condition: ContextKeyExpr.equals('config.notebook.outline.showNonHeaderMarkdownCells', true) + }, + menu: { + id: MenuId.NotebookOutlineFilter, + group: 'filter', + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const configurationService = accessor.get(IConfigurationService); + const showNonHeaderMarkdownCells = configurationService.getValue('notebook.outline.showNonHeaderMarkdownCells'); + configurationService.updateValue('notebook.outline.showNonHeaderMarkdownCells', !showNonHeaderMarkdownCells); + } +}); diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index 0ffd61f705a..31c62508ca9 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -22,6 +22,7 @@ import { ApplyCodeActionReason, applyCodeAction, getCodeActions } from 'vs/edito import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -109,12 +110,14 @@ class TrimWhitespaceParticipant implements IStoredFileWorkingCopySaveParticipant ) { } async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, _token: CancellationToken): Promise { - if (this.configurationService.getValue('files.trimTrailingWhitespace')) { - await this.doTrimTrailingWhitespace(workingCopy, context.reason === SaveReason.AUTO, progress); + const trimTrailingWhitespaceOption = this.configurationService.getValue('files.trimTrailingWhitespace'); + const trimInRegexAndStrings = this.configurationService.getValue('files.trimTrailingWhitespaceInRegexAndStrings'); + if (trimTrailingWhitespaceOption) { + await this.doTrimTrailingWhitespace(workingCopy, context.reason === SaveReason.AUTO, trimInRegexAndStrings, progress); } } - private async doTrimTrailingWhitespace(workingCopy: IStoredFileWorkingCopy, isAutoSaved: boolean, progress: IProgress) { + private async doTrimTrailingWhitespace(workingCopy: IStoredFileWorkingCopy, isAutoSaved: boolean, trimInRegexesAndStrings: boolean, progress: IProgress) { if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) { return; } @@ -149,7 +152,7 @@ class TrimWhitespaceParticipant implements IStoredFileWorkingCopySaveParticipant } } - const ops = trimTrailingWhitespace(model, cursors); + const ops = trimTrailingWhitespace(model, cursors, trimInRegexesAndStrings); if (!ops.length) { return []; // Nothing to do } @@ -224,8 +227,11 @@ class TrimFinalNewLinesParticipant implements IStoredFileWorkingCopySaveParticip const textBuffer = cell.textBuffer; const lastNonEmptyLine = this.findLastNonEmptyLine(textBuffer); const deleteFromLineNumber = Math.max(lastNonEmptyLine + 1, cannotTouchLineNumber + 1); - const deletionRange = new Range(deleteFromLineNumber, 1, textBuffer.getLineCount(), textBuffer.getLineLastNonWhitespaceColumn(textBuffer.getLineCount())); + if (deleteFromLineNumber > textBuffer.getLineCount()) { + return; + } + const deletionRange = new Range(deleteFromLineNumber, 1, textBuffer.getLineCount(), textBuffer.getLineLastNonWhitespaceColumn(textBuffer.getLineCount())); if (deletionRange.isEmpty()) { return; } @@ -244,7 +250,7 @@ class TrimFinalNewLinesParticipant implements IStoredFileWorkingCopySaveParticip } } -class FinalNewLineParticipant implements IStoredFileWorkingCopySaveParticipant { +class InsertFinalNewLineParticipant implements IStoredFileWorkingCopySaveParticipant { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @@ -419,8 +425,8 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } - private createCodeActionsOnSave(settingItems: readonly string[]): CodeActionKind[] { - const kinds = settingItems.map(x => new CodeActionKind(x)); + private createCodeActionsOnSave(settingItems: readonly string[]): HierarchicalKind[] { + const kinds = settingItems.map(x => new HierarchicalKind(x)); // Remove subsets return kinds.filter(kind => { @@ -428,7 +434,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa }); } - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken): Promise { + private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly HierarchicalKind[], excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken): Promise { const getActionProgress = new class implements IProgress { private _names = new Set(); @@ -491,7 +497,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } - private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken) { + private getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken) { return getCodeActions(this.languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.OnSave, @@ -520,7 +526,7 @@ export class SaveParticipantsContribution extends Disposable implements IWorkben this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(TrimWhitespaceParticipant))); this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(CodeActionOnSaveParticipant))); this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(FormatOnSaveParticipant))); - this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(FinalNewLineParticipant))); + this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(InsertFinalNewLineParticipant))); this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(TrimFinalNewLinesParticipant))); } } diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 430062261c9..c6a8232e5d4 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -16,10 +16,10 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; -import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { CELL_TITLE_CELL_GROUP_ID, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_GENERATED_BY_CHAT, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; registerAction2(class extends NotebookAction { @@ -30,7 +30,7 @@ registerAction2(class extends NotebookAction { title: localize2('notebook.cell.chat.accept', "Make Request"), icon: Codicon.send, keybinding: { - when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, NOTEBOOK_CELL_EDITOR_FOCUSED.negate()), weight: KeybindingWeight.EditorCore + 7, primary: KeyCode.Enter }, @@ -59,6 +59,7 @@ registerAction2(class extends NotebookCellAction { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, + NOTEBOOK_CELL_EDITOR_FOCUSED.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate() ), weight: KeybindingWeight.EditorCore + 7, @@ -99,6 +100,7 @@ registerAction2(class extends NotebookAction { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_LAST, + NOTEBOOK_CELL_EDITOR_FOCUSED.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate() ), weight: KeybindingWeight.EditorCore + 7, @@ -209,7 +211,7 @@ registerAction2(class extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.dismiss(); + NotebookChatController.get(context.notebookEditor)?.dismiss(false); } }); @@ -224,12 +226,12 @@ registerAction2(class extends NotebookAction { tooltip: localize('apply3', 'Accept Changes'), keybinding: [ { - when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, NOTEBOOK_CELL_EDITOR_FOCUSED.negate()), weight: KeybindingWeight.EditorContrib + 10, primary: KeyMod.CtrlCmd | KeyCode.Enter, }, { - when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT), + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, NOTEBOOK_CELL_EDITOR_FOCUSED.negate()), weight: KeybindingWeight.EditorCore + 10, primary: KeyCode.Escape }, @@ -237,6 +239,7 @@ registerAction2(class extends NotebookAction { when: ContextKeyExpr.and( NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), + NOTEBOOK_CELL_EDITOR_FOCUSED.negate(), CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION.isEqualTo('below') ), primary: KeyMod.CtrlCmd | KeyCode.Enter, @@ -267,7 +270,7 @@ registerAction2(class extends NotebookAction { title: localize('discard', 'Discard'), icon: Codicon.discard, keybinding: { - when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT.negate()), + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT.negate(), NOTEBOOK_CELL_EDITOR_FOCUSED.negate()), weight: KeybindingWeight.EditorContrib, primary: KeyCode.Escape }, @@ -353,14 +356,14 @@ registerAction2(class extends NotebookAction { constructor() { super( { - id: 'notebook.cell.insertCodeCellWithChat', + id: 'notebook.cell.chat.start', title: { value: '$(sparkle) ' + localize('notebookActions.menu.insertCodeCellWithChat', "Generate"), original: '$(sparkle) Generate', }, - tooltip: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Generate Code Cell with Chat"), + tooltip: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Start Chat to Generate Code"), metadata: { - description: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Generate Code Cell with Chat"), + description: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Start Chat to Generate Code"), args: [ { name: 'args', @@ -449,12 +452,12 @@ registerAction2(class extends NotebookAction { constructor() { super( { - id: 'notebook.cell.insertCodeCellWithChatAtTop', + id: 'notebook.cell.chat.startAtTop', title: { value: '$(sparkle) ' + localize('notebookActions.menu.insertCodeCellWithChat', "Generate"), original: '$(sparkle) Generate', }, - tooltip: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Generate Code Cell with Chat"), + tooltip: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Start Chat to Generate Code"), f1: false, menu: [ { @@ -479,10 +482,10 @@ registerAction2(class extends NotebookAction { MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { command: { - id: 'notebook.cell.insertCodeCellWithChat', + id: 'notebook.cell.chat.start', icon: Codicon.sparkle, title: localize('notebookActions.menu.insertCode.ontoolbar', "Generate"), - tooltip: localize('notebookActions.menu.insertCode.tooltip', "Generate Code Cell with Chat") + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Start Chat to Generate Code") }, order: -10, group: 'navigation/add', @@ -573,3 +576,86 @@ registerAction2(class extends NotebookAction { NotebookChatController.get(context.notebookEditor)?.focusAbove(); } }); + +registerAction2(class extends NotebookAction { + constructor() { + super( + { + id: 'notebook.cell.chat.previousFromHistory', + title: localize2('notebook.cell.chat.previousFromHistory', "Previous From History"), + precondition: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + keybinding: { + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.UpArrow, + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { + NotebookChatController.get(context.notebookEditor)?.populateHistory(true); + } +}); + +registerAction2(class extends NotebookAction { + constructor() { + super( + { + id: 'notebook.cell.chat.nextFromHistory', + title: localize2('notebook.cell.chat.nextFromHistory', "Next From History"), + precondition: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + keybinding: { + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.DownArrow + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { + NotebookChatController.get(context.notebookEditor)?.populateHistory(false); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.restore', + title: localize2('notebookActions.restoreCellprompt', "Generate"), + icon: Codicon.sparkle, + menu: { + id: MenuId.NotebookCellTitle, + group: CELL_TITLE_CELL_GROUP_ID, + order: 0, + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + CTX_INLINE_CHAT_HAS_PROVIDER, + NOTEBOOK_CELL_GENERATED_BY_CHAT, + ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) + ) + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const cell = context.cell; + + if (!cell) { + return; + } + + const notebookEditor = context.notebookEditor; + const controller = NotebookChatController.get(notebookEditor); + + if (!controller) { + return; + } + + const prompt = controller.getPromptFromCache(cell); + + if (prompt) { + controller.restore(cell, prompt); + } + } +}); diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 93a98b32d60..6bb84839d84 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -6,13 +6,15 @@ import { Dimension, IFocusTracker, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame, trackFocus } from 'vs/base/browser/dom'; import { CancelablePromise, Queue, createCancelablePromise, disposableTimeout, raceCancellationError } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { LRUCache } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; @@ -29,7 +31,9 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { AsyncProgress } from 'vs/platform/progress/common/progress'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { SaveReason } from 'vs/workbench/common/editor'; +import { GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; @@ -40,16 +44,11 @@ import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/in import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; -import { INotebookEditor, INotebookEditorContribution, INotebookViewZone, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookViewZone } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; - - -const WIDGET_MARGIN_BOTTOM = 16; - class NotebookChatWidget extends Disposable implements INotebookViewZone { set afterModelPosition(afterModelPosition: number) { this.notebookViewZone.afterModelPosition = afterModelPosition; @@ -67,7 +66,7 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { return this.notebookViewZone.heightInPx; } - private _editingCell: CellViewModel | null = null; + private _editingCell: ICellViewModel | null = null; get editingCell() { return this._editingCell; @@ -86,7 +85,7 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { super(); this._register(inlineChatWidget.onDidChangeHeight(() => { - this.heightInPx = inlineChatWidget.getHeight() + WIDGET_MARGIN_BOTTOM; + this.heightInPx = inlineChatWidget.getHeight(); this._notebookEditor.changeViewZones(accessor => { accessor.layoutZone(id); }); @@ -96,21 +95,48 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { this._layoutWidget(inlineChatWidget, widgetContainer); } + restoreEditingCell(initEditingCell: ICellViewModel) { + this._editingCell = initEditingCell; + + const decorationIds = this._notebookEditor.deltaCellDecorations([], [{ + handle: this._editingCell.handle, + options: { className: 'nb-chatGenerationHighlight', outputClassName: 'nb-chatGenerationHighlight' } + }]); + + this._register(toDisposable(() => { + this._notebookEditor.deltaCellDecorations(decorationIds, []); + })); + } + + hasFocus() { + return this.inlineChatWidget.hasFocus(); + } + focus() { + this.updateNotebookEditorFocusNSelections(); this.inlineChatWidget.focus(); } + updateNotebookEditorFocusNSelections() { + this._notebookEditor.focusContainer(true); + this._notebookEditor.setFocus({ start: this.afterModelPosition, end: this.afterModelPosition }); + this._notebookEditor.setSelections([{ + start: this.afterModelPosition, + end: this.afterModelPosition + }]); + } + getEditingCell() { return this._editingCell; } - async getOrCreateEditingCell(): Promise<{ cell: CellViewModel; editor: IActiveCodeEditor } | undefined> { + async getOrCreateEditingCell(): Promise<{ cell: ICellViewModel; editor: IActiveCodeEditor } | undefined> { if (this._editingCell) { - await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor'); - if (this._notebookEditor.activeCodeEditor?.hasModel()) { + const codeEditor = this._notebookEditor.codeEditors.find(ce => ce[0] === this._editingCell)?.[1]; + if (codeEditor?.hasModel()) { return { cell: this._editingCell, - editor: this._notebookEditor.activeCodeEditor + editor: codeEditor }; } else { return undefined; @@ -121,17 +147,35 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { return undefined; } + const widgetHasFocus = this.inlineChatWidget.hasFocus(); + this._editingCell = insertCell(this._languageService, this._notebookEditor, this.afterModelPosition, CellKind.Code, 'above'); if (!this._editingCell) { return undefined; } - await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor', { revealBehavior: ScrollToRevealBehavior.firstLine }); - if (this._notebookEditor.activeCodeEditor?.hasModel()) { + await this._notebookEditor.revealFirstLineIfOutsideViewport(this._editingCell); + + // update decoration + const decorationIds = this._notebookEditor.deltaCellDecorations([], [{ + handle: this._editingCell.handle, + options: { className: 'nb-chatGenerationHighlight', outputClassName: 'nb-chatGenerationHighlight' } + }]); + + this._register(toDisposable(() => { + this._notebookEditor.deltaCellDecorations(decorationIds, []); + })); + + if (widgetHasFocus) { + this.focus(); + } + + const codeEditor = this._notebookEditor.codeEditors.find(ce => ce[0] === this._editingCell)?.[1]; + if (codeEditor?.hasModel()) { return { cell: this._editingCell, - editor: this._notebookEditor.activeCodeEditor + editor: codeEditor }; } @@ -149,10 +193,10 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { const layoutConfiguration = this._notebookEditor.notebookOptions.getLayoutConfiguration(); const rightMargin = layoutConfiguration.cellRightMargin; const leftMargin = this._notebookEditor.notebookOptions.getCellEditorContainerLeftMargin(); - const maxWidth = !inlineChatWidget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; + const maxWidth = 640; const width = Math.min(maxWidth, this._notebookEditor.getLayoutInfo().width - leftMargin - rightMargin); - inlineChatWidget.layout(new Dimension(width, 80 + WIDGET_MARGIN_BOTTOM)); + inlineChatWidget.layout(new Dimension(width, this.heightInPx)); inlineChatWidget.domNode.style.width = `${width}px`; widgetContainer.style.left = `${leftMargin}px`; } @@ -166,6 +210,20 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { } } +export interface INotebookCellTextModelLike { uri: URI; viewType: string } +class NotebookCellTextModelLikeId { + static str(k: INotebookCellTextModelLike): string { + return `${k.viewType}/${k.uri.toString()}`; + } + static obj(s: string): INotebookCellTextModelLike { + const idx = s.indexOf('/'); + return { + viewType: s.substring(0, idx), + uri: URI.parse(s.substring(idx + 1)) + }; + } +} + export class NotebookChatController extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.chatController'; static counter: number = 0; @@ -173,6 +231,17 @@ export class NotebookChatController extends Disposable implements INotebookEdito public static get(editor: INotebookEditor): NotebookChatController | null { return editor.getContribution(NotebookChatController.id); } + + // History + private static _storageKey = 'inline-chat-history'; + private static _promptHistory: string[] = []; + private _historyOffset: number = -1; + private _historyCandidate: string = ''; + private _historyUpdate: (prompt: string) => void; + private _promptCache = new LRUCache(1000, 0.7); + private readonly _onDidChangePromptCache = this._register(new Emitter<{ cell: URI }>()); + readonly onDidChangePromptCache = this._onDidChangePromptCache.event; + private _strategy: EditStrategy | undefined; private _sessionCtor: CancelablePromise | undefined; private _activeSession?: Session; @@ -198,6 +267,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito @IModelService private readonly _modelService: IModelService, @ILanguageService private readonly _languageService: ILanguageService, @INotebookExecutionStateService private _executionStateService: INotebookExecutionStateService, + @IStorageService private readonly _storageService: IStorageService, ) { super(); @@ -208,6 +278,18 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._ctxOuterFocusPosition = CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION.bindTo(this._contextKeyService); this._registerFocusTracker(); + + NotebookChatController._promptHistory = JSON.parse(this._storageService.get(NotebookChatController._storageKey, StorageScope.PROFILE, '[]')); + this._historyUpdate = (prompt: string) => { + const idx = NotebookChatController._promptHistory.indexOf(prompt); + if (idx >= 0) { + NotebookChatController._promptHistory.splice(idx, 1); + } + NotebookChatController._promptHistory.unshift(prompt); + this._historyOffset = -1; + this._historyCandidate = ''; + this._storageService.store(NotebookChatController._storageKey, JSON.stringify(NotebookChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); + }; } private _registerFocusTracker() { @@ -232,28 +314,60 @@ export class NotebookChatController extends Disposable implements INotebookEdito run(index: number, input: string | undefined, autoSend: boolean | undefined): void { if (this._widget) { - if (this._widget.afterModelPosition === index) { - // this._chatZone - // chatZone focus - } else { + if (this._widget.afterModelPosition !== index) { + this._disposeWidget(); const window = getWindow(this._widget.domNode); - this._widget.dispose(); - this._widget = undefined; - this._widgetDisposableStore.clear(); scheduleAtNextAnimationFrame(window, () => { - this._createWidget(index, input, autoSend); + this._createWidget(index, input, autoSend, undefined); }); } return; } - this._createWidget(index, input, autoSend); + this._createWidget(index, input, autoSend, undefined); // TODO: reveal widget to the center if it's out of the viewport } - private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined) { + restore(editingCell: ICellViewModel, input: string) { + if (!this._notebookEditor.hasModel()) { + return; + } + + const index = this._notebookEditor.textModel.cells.indexOf(editingCell.model); + + if (index < 0) { + return; + } + + if (this._widget) { + if (this._widget.afterModelPosition !== index) { + this._disposeWidget(); + const window = getWindow(this._widget.domNode); + + scheduleAtNextAnimationFrame(window, () => { + this._createWidget(index, input, false, editingCell); + }); + } + + return; + } + + this._createWidget(index, input, false, editingCell); + } + + private _disposeWidget() { + this._widget?.dispose(); + this._widget = undefined; + this._widgetDisposableStore.clear(); + + this._historyOffset = -1; + this._historyCandidate = ''; + } + + + private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined, initEditingCell: ICellViewModel | undefined) { if (!this._notebookEditor.hasModel()) { return; } @@ -290,9 +404,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito const inlineChatWidget = this._widgetDisposableStore.add(this._instantiationService.createInstance( InlineChatWidget, - fakeParentEditor, { - menuId: MENU_CELL_CHAT_INPUT, + telemetrySource: 'notebook-generate-cell', + inputMenuId: MENU_CELL_CHAT_INPUT, widgetMenuId: MENU_CELL_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK @@ -309,7 +423,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._notebookEditor.changeViewZones(accessor => { const notebookViewZone = { afterModelPosition: index, - heightInPx: 80 + WIDGET_MARGIN_BOTTOM, + heightInPx: 80, domNode: viewZoneContainer }; @@ -327,6 +441,11 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._languageService ); + if (initEditingCell) { + this._widget.restoreEditingCell(initEditingCell); + this._updateUserEditingState(); + } + this._ctxCellWidgetFocused.set(true); disposableTimeout(() => { @@ -389,12 +508,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito return; } - this._notebookEditor.focusContainer(true); - this._notebookEditor.setFocus({ start: this._widget.afterModelPosition, end: this._widget.afterModelPosition }); - this._notebookEditor.setSelections([{ - start: this._widget.afterModelPosition, - end: this._widget.afterModelPosition - }]); + this._widget.updateNotebookEditorFocusNSelections(); } async acceptInput() { @@ -407,6 +521,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito assertType(this._activeSession.lastInput); const value = this._activeSession.lastInput.value; + + this._historyUpdate(value); + const editor = this._widget.parentEditor; const model = editor.getModel(); @@ -512,7 +629,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._widget?.inlineChatWidget.updateChatMessage(undefined); this._widget?.inlineChatWidget.updateFollowUps(undefined); this._widget?.inlineChatWidget.updateProgress(true); - this._widget?.inlineChatWidget.updateInfo(!this._activeSession.lastExchange ? localize('thinking', "Thinking\u2026") : ''); + this._widget?.inlineChatWidget.updateInfo(!this._activeSession.lastExchange ? GeneratingPhrase + '\u2026' : ''); this._ctxHasActiveRequest.set(true); const reply = await raceCancellationError(Promise.resolve(task), this._activeRequestCts.token); @@ -697,12 +814,21 @@ export class NotebookChatController extends Disposable implements INotebookEdito return; } + const editingCell = this._widget?.getEditingCell(); + + if (editingCell && this._notebookEditor.hasModel() && this._activeSession.lastInput) { + const cellId = NotebookCellTextModelLikeId.str({ uri: editingCell.uri, viewType: this._notebookEditor.textModel.viewType }); + const prompt = this._activeSession.lastInput.value; + this._promptCache.set(cellId, prompt); + this._onDidChangePromptCache.fire({ cell: editingCell.uri }); + } + try { await this._strategy.apply(editor); this._inlineChatSessionService.releaseSession(this._activeSession); } catch (_err) { } - this.dismiss(); + this.dismiss(false); } async focusAbove() { @@ -738,6 +864,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito await this._notebookEditor.focusNotebookCell(cell, 'editor'); } + hasFocus() { + return this._widget?.hasFocus() ?? false; + } + focus() { this._focusWidget(); } @@ -759,6 +889,39 @@ export class NotebookChatController extends Disposable implements INotebookEdito } } + populateHistory(up: boolean) { + if (!this._widget) { + return; + } + + const len = NotebookChatController._promptHistory.length; + if (len === 0) { + return; + } + + if (this._historyOffset === -1) { + // remember the current value + this._historyCandidate = this._widget.inlineChatWidget.value; + } + + const newIdx = this._historyOffset + (up ? 1 : -1); + if (newIdx >= len) { + // reached the end + return; + } + + let entry: string; + if (newIdx < 0) { + entry = this._historyCandidate; + this._historyOffset = -1; + } else { + entry = NotebookChatController._promptHistory[newIdx]; + this._historyOffset = newIdx; + } + + this._widget.inlineChatWidget.value = entry; + this._widget.inlineChatWidget.selectAll(); + } async cancelCurrentRequest(discard: boolean) { if (discard) { @@ -768,11 +931,15 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._activeRequestCts?.cancel(); } + getEditingCell() { + return this._widget?.getEditingCell(); + } + discard() { this._strategy?.cancel(); this._activeRequestCts?.cancel(); this._widget?.discardChange(); - this.dismiss(); + this.dismiss(true); } async feedbackLast(kind: InlineChatResponseFeedbackKind) { @@ -783,25 +950,25 @@ export class NotebookChatController extends Disposable implements INotebookEdito } - dismiss() { - // move focus back to the cell above - if (this._widget) { - const widgetIndex = this._widget.afterModelPosition; - const currentFocus = this._notebookEditor.getFocus(); - - if (currentFocus.start === widgetIndex && currentFocus.end === widgetIndex) { - // focus is on the widget - if (widgetIndex === 0) { - // on top of all cells - if (this._notebookEditor.getLength() > 0) { - this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); - } - } else { - const cell = this._notebookEditor.cellAt(widgetIndex - 1); - if (cell) { - this._notebookEditor.focusNotebookCell(cell, 'container'); - } - } + dismiss(discard: boolean) { + const widget = this._widget; + const widgetIndex = widget?.afterModelPosition; + const currentFocus = this._notebookEditor.getFocus(); + const isWidgetFocused = currentFocus.start === widgetIndex && currentFocus.end === widgetIndex; + + if (widget && isWidgetFocused) { + // change focus only when the widget is focused + const editingCell = widget.getEditingCell(); + const shouldFocusEditingCell = editingCell && !discard; + const shouldFocusTopCell = widgetIndex === 0 && this._notebookEditor.getLength() > 0; + const shouldFocusAboveCell = widgetIndex !== 0 && this._notebookEditor.cellAt(widgetIndex - 1); + + if (shouldFocusEditingCell) { + this._notebookEditor.focusNotebookCell(editingCell, 'container'); + } else if (shouldFocusTopCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); + } else if (shouldFocusAboveCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(widgetIndex - 1)!, 'container'); } } @@ -814,9 +981,29 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._widgetDisposableStore.clear(); } - public override dispose(): void { - this.dismiss(); + // check if a cell is generated by prompt by checking prompt cache + isCellGeneratedByChat(cell: ICellViewModel) { + if (!this._notebookEditor.hasModel()) { + // no model attached yet + return false; + } + const cellId = NotebookCellTextModelLikeId.str({ uri: cell.uri, viewType: this._notebookEditor.textModel.viewType }); + return this._promptCache.has(cellId); + } + + // get prompt from cache + getPromptFromCache(cell: ICellViewModel) { + if (!this._notebookEditor.hasModel()) { + // no model attached yet + return undefined; + } + + const cellId = NotebookCellTextModelLikeId.str({ uri: cell.uri, viewType: this._notebookEditor.textModel.viewType }); + return this._promptCache.get(cellId); + } + public override dispose(): void { + this.dismiss(false); super.dispose(); } } @@ -896,4 +1083,3 @@ export class EditStrategy { registerNotebookContribution(NotebookChatController.id, NotebookChatController); - diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 087c143716c..3cc7faf8354 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -31,6 +31,7 @@ export const CELL_TITLE_CELL_GROUP_ID = 'inline/cell'; export const CELL_TITLE_OUTPUT_GROUP_ID = 'inline/output'; export const NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT = KeybindingWeight.EditorContrib; // smaller than Suggest Widget, etc +export const NOTEBOOK_OUTPUT_WEBVIEW_ACTION_WEIGHT = KeybindingWeight.WorkbenchContrib + 1; // higher than Workbench contribution (such as Notebook List View), etc export const enum CellToolbarOrder { EditCell, diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 2ca62a92f6f..ce41420deba 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -20,6 +20,8 @@ import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { CELL_TITLE_CELL_GROUP_ID, CellToolbarOrder, INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, NotebookAction, NotebookCellAction, NotebookMultiCellAction, cellExecutionArgs, executeNotebookCondition, getContextFromActiveEditor, getContextFromUri, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, IFocusNotebookCellOptions, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -198,7 +200,10 @@ registerAction2(class ExecuteCell extends NotebookMultiCellAction { precondition: executeThisCellCondition, title: localize('notebookActions.execute', "Execute Cell"), keybinding: { - when: NOTEBOOK_CELL_LIST_FOCUSED, + when: ContextKeyExpr.or( + NOTEBOOK_CELL_LIST_FOCUSED, + ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED) + ), primary: KeyMod.WinCtrl | KeyCode.Enter, win: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter @@ -229,6 +234,21 @@ registerAction2(class ExecuteCell extends NotebookMultiCellAction { await context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); } + const chatController = NotebookChatController.get(context.notebookEditor); + const editingCell = chatController?.getEditingCell(); + if (chatController?.hasFocus() && editingCell) { + const group = editorGroupsService.activeGroup; + + if (group) { + if (group.activeEditor) { + group.pinEditor(group.activeEditor); + } + } + + await context.notebookEditor.executeNotebookCells([editingCell]); + return; + } + await runCell(editorGroupsService, context); } }); diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts index 2dfc3c83bc3..19f30d4185f 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts @@ -6,9 +6,10 @@ import { Codicon } from 'vs/base/common/codicons'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize, localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -242,3 +243,35 @@ registerAction2(class NotebookWebviewResetAction extends Action2 { } } }); + +registerAction2(class ToggleNotebookStickyScroll extends Action2 { + constructor() { + super({ + id: 'notebook.action.toggleNotebookStickyScroll', + title: { + ...localize2('toggleStickyScroll', "Toggle Notebook Sticky Scroll"), + mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), + }, + category: Categories.View, + toggled: { + condition: ContextKeyExpr.equals('config.notebook.stickyScroll.enabled', true), + title: localize('notebookStickyScroll', "Toggle Notebook Sticky Scroll"), + mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), + }, + menu: [ + { id: MenuId.CommandPalette }, + { + id: MenuId.NotebookStickyScrollContext, + group: 'notebookView', + order: 2 + } + ] + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const newValue = !configurationService.getValue('notebook.stickyScroll.enabled'); + return configurationService.updateValue('notebook.stickyScroll.enabled', newValue); + } +}); diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts new file mode 100644 index 00000000000..b4831209b27 --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize, localize2 } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookOutlineContext } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; +import { FoldingController } from 'vs/workbench/contrib/notebook/browser/controller/foldingController'; +import { CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; + +export type NotebookSectionArgs = { + notebookEditor: INotebookEditor | undefined; + outlineEntry: OutlineEntry; +}; + +export type ValidNotebookSectionArgs = { + notebookEditor: INotebookEditor; + outlineEntry: OutlineEntry; +}; + +export class NotebookRunSingleCellInSection extends Action2 { + constructor() { + super({ + id: 'notebook.section.runSingleCell', + title: { + ...localize2('runCell', "Run Cell"), + mnemonicTitle: localize({ key: 'mirunCell', comment: ['&& denotes a mnemonic'] }, "&&Run Cell"), + }, + shortTitle: localize('runCell', "Run Cell"), + icon: icons.executeIcon, + menu: [ + { + id: MenuId.NotebookOutlineActionMenu, + group: 'inline', + order: 1, + when: ContextKeyExpr.and( + NotebookOutlineContext.CellKind.isEqualTo(CellKind.Code), + NotebookOutlineContext.OutlineElementTarget.isEqualTo(OutlineTarget.OutlinePane), + NotebookOutlineContext.CellHasChildren.toNegated(), + NotebookOutlineContext.CellHasHeader.toNegated(), + ) + } + ] + }); + } + + override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { + if (!checkSectionContext(context)) { + return; + } + + context.notebookEditor.executeNotebookCells([context.outlineEntry.cell]); + } +} + +export class NotebookRunCellsInSection extends Action2 { + constructor() { + super({ + id: 'notebook.section.runCells', + title: { + ...localize2('runCellsInSection', "Run Cells In Section"), + mnemonicTitle: localize({ key: 'mirunCellsInSection', comment: ['&& denotes a mnemonic'] }, "&&Run Cells In Section"), + }, + shortTitle: localize('runCellsInSection', "Run Cells In Section"), + // icon: icons.executeBelowIcon, // TODO @Yoyokrazy replace this with new icon later + menu: [ + { + id: MenuId.NotebookStickyScrollContext, + group: 'notebookExecution', + order: 1 + }, + { + id: MenuId.NotebookOutlineActionMenu, + group: 'inline', + order: 1, + when: ContextKeyExpr.and( + NotebookOutlineContext.CellKind.isEqualTo(CellKind.Markup), + NotebookOutlineContext.OutlineElementTarget.isEqualTo(OutlineTarget.OutlinePane), + NotebookOutlineContext.CellHasChildren, + NotebookOutlineContext.CellHasHeader, + ) + } + ] + }); + } + + override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { + if (!checkSectionContext(context)) { + return; + } + + const cell = context.outlineEntry.cell; + const idx = context.notebookEditor.getViewModel()?.getCellIndex(cell); + if (idx === undefined) { + return; + } + const length = context.notebookEditor.getViewModel()?.getFoldedLength(idx); + if (length === undefined) { + return; + } + + const cells = context.notebookEditor.getCellsInRange({ start: idx, end: idx + length + 1 }); + context.notebookEditor.executeNotebookCells(cells); + } +} + +export class NotebookFoldSection extends Action2 { + constructor() { + super({ + id: 'notebook.section.foldSection', + title: { + ...localize2('foldSection', "Fold Section"), + mnemonicTitle: localize({ key: 'mifoldSection', comment: ['&& denotes a mnemonic'] }, "&&Fold Section"), + }, + shortTitle: localize('foldSection', "Fold Section"), + menu: [ + { + id: MenuId.NotebookOutlineActionMenu, + group: 'notebookFolding', + order: 2, + when: ContextKeyExpr.and( + NotebookOutlineContext.CellKind.isEqualTo(CellKind.Markup), + NotebookOutlineContext.OutlineElementTarget.isEqualTo(OutlineTarget.OutlinePane), + NotebookOutlineContext.CellHasChildren, + NotebookOutlineContext.CellHasHeader, + NotebookOutlineContext.CellFoldingState.isEqualTo(CellFoldingState.Expanded) + ) + } + ] + }); + } + + override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { + if (!checkSectionContext(context)) { + return; + } + + this.toggleFoldRange(context.outlineEntry, context.notebookEditor); + } + + private toggleFoldRange(entry: OutlineEntry, notebookEditor: INotebookEditor) { + const foldingController = notebookEditor.getContribution(FoldingController.id); + const index = entry.index; + const headerLevel = entry.level; + const newFoldingState = CellFoldingState.Collapsed; + + foldingController.setFoldingStateDown(index, newFoldingState, headerLevel); + } +} + +export class NotebookExpandSection extends Action2 { + constructor() { + super({ + id: 'notebook.section.expandSection', + title: { + ...localize2('expandSection', "Expand Section"), + mnemonicTitle: localize({ key: 'miexpandSection', comment: ['&& denotes a mnemonic'] }, "&&Expand Section"), + }, + shortTitle: localize('expandSection', "Expand Section"), + menu: [ + { + id: MenuId.NotebookOutlineActionMenu, + group: 'notebookFolding', + order: 2, + when: ContextKeyExpr.and( + NotebookOutlineContext.CellKind.isEqualTo(CellKind.Markup), + NotebookOutlineContext.OutlineElementTarget.isEqualTo(OutlineTarget.OutlinePane), + NotebookOutlineContext.CellHasChildren, + NotebookOutlineContext.CellHasHeader, + NotebookOutlineContext.CellFoldingState.isEqualTo(CellFoldingState.Collapsed) + ) + } + ] + }); + } + + override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { + if (!checkSectionContext(context)) { + return; + } + + this.toggleFoldRange(context.outlineEntry, context.notebookEditor); + } + + private toggleFoldRange(entry: OutlineEntry, notebookEditor: INotebookEditor) { + const foldingController = notebookEditor.getContribution(FoldingController.id); + const index = entry.index; + const headerLevel = entry.level; + const newFoldingState = CellFoldingState.Expanded; + + foldingController.setFoldingStateDown(index, newFoldingState, headerLevel); + } +} + +/** + * Take in context args and check if they exist + * + * @param context - Notebook Section Context containing a notebook editor and outline entry + * @returns true if context is valid, false otherwise + */ +function checkSectionContext(context: NotebookSectionArgs): context is ValidNotebookSectionArgs { + return !!(context && context.notebookEditor && context.outlineEntry); +} + +registerAction2(NotebookRunSingleCellInSection); +registerAction2(NotebookRunCellsInSection); +registerAction2(NotebookFoldSection); +registerAction2(NotebookExpandSection); diff --git a/code/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/code/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index e2de1a22096..29dc02777d3 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -9,7 +9,7 @@ import { findLastIdx } from 'vs/base/common/arraysFind'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, IEditorPaneWithSelection } from 'vs/workbench/common/editor'; +import { EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling, IEditorPaneWithSelection } from 'vs/workbench/common/editor'; import { getDefaultNotebookCreationOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../../common/notebookDiffEditorInput'; @@ -47,7 +47,6 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookDiffOverviewRuler } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler'; import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistry'; -import { mainWindow } from 'vs/base/browser/window'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const $ = DOM.$; @@ -86,7 +85,7 @@ class NotebookDiffEditorSelection implements IEditorPaneSelection { } } -export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor, INotebookDelegateForWebview, IEditorPaneWithSelection { +export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor, INotebookDelegateForWebview, IEditorPaneWithSelection, IEditorPaneWithScrolling { public static readonly ENTIRE_DIFF_OVERVIEW_WIDTH = 30; creationOptions: INotebookEditorCreationOptions = getDefaultNotebookCreationOptions(); static readonly ID: string = NOTEBOOK_DIFF_EDITOR_ID; @@ -108,6 +107,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD public readonly onMouseUp = this._onMouseUp.event; private readonly _onDidScroll = this._register(new Emitter()); readonly onDidScroll: Event = this._onDidScroll.event; + readonly onDidChangeScroll: Event = this._onDidScroll.event; private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined; protected _scopeContextKeyService!: IContextKeyService; private _model: INotebookDiffEditorModel | null = null; @@ -143,6 +143,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } constructor( + group: IEditorGroup, @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -153,8 +154,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, @ICodeEditorService codeEditorService: ICodeEditorService ) { - super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); - this._notebookOptions = new NotebookOptions(DOM.getWindowById(this.group?.windowId, true).window ?? mainWindow, this.configurationService, notebookExecutionStateService, codeEditorService, false); + super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); + this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false); this._register(this._notebookOptions); this._revealFirst = true; } @@ -168,9 +169,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } private createFontInfo() { - const window = DOM.getWindowById(this.group?.windowId, true).window; const editorOptions = this.configurationService.getValue('editor'); - return FontMeasurements.readFontInfo(window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(window).value)); + return FontMeasurements.readFontInfo(this.window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(this.window).value)); } private isOverviewRulerEnabled(): boolean { @@ -210,6 +210,24 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return this._list?.scrollHeight ?? 0; } + getScrollPosition(): IEditorPaneScrollPosition { + return { + scrollTop: this.getScrollTop(), + scrollLeft: this._list?.scrollLeft ?? 0 + }; + } + + setScrollPosition(scrollPosition: IEditorPaneScrollPosition): void { + if (!this._list) { + return; + } + + this._list.scrollTop = scrollPosition.scrollTop; + if (scrollPosition.scrollLeft !== undefined) { + this._list.scrollLeft = scrollPosition.scrollLeft; + } + } + delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent) { this._list?.delegateVerticalScrollbarPointerDown(browserEvent); } @@ -271,7 +289,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD NotebookTextDiffList, 'NotebookTextDiff', this._listViewContainer, - this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, DOM.getWindow(this._listViewContainer)), + this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, this.window), renderers, this.contextKeyService, { @@ -462,7 +480,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _attachModel() { this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); const updateInsets = () => { - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { if (this._isDisposed) { return; } @@ -499,7 +517,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); - this._modifiedWebview.createWebview(DOM.getActiveWindow()); + this._modifiedWebview.createWebview(this.window); this._modifiedWebview.element.style.width = `calc(50% - 16px)`; this._modifiedWebview.element.style.left = `calc(50%)`; } @@ -516,7 +534,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); - this._originalWebview.createWebview(DOM.getActiveWindow()); + this._originalWebview.createWebview(this.window); this._originalWebview.element.style.width = `calc(50% - 16px)`; this._originalWebview.element.style.left = `16px`; } @@ -776,7 +794,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const webview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { webview?.ackHeight([{ cellId: cellInfo.cellId, outputId, height }]); }, 10); } @@ -794,7 +812,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } let r: () => void; - const layoutDisposable = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(this.window, () => { this.pendingLayouts.delete(cell); relayout(cell, height); @@ -978,10 +996,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return this; } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - } - override clearInput(): void { super.clearInput(); diff --git a/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 309b3b4ef84..93989cbc04f 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/code/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -512,3 +512,9 @@ } .monaco-workbench .notebookOverlay .codicon-debug-continue { color: var(--vscode-icon-foreground) !important; } + +/** Cell Chat **/ +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-chatGenerationHighlight .cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row.nb-chatGenerationHighlight { + background-color: var(--vscode-notebook-selectedCellBackground) !important; +} diff --git a/code/src/vs/workbench/contrib/notebook/browser/media/notebookFolding.css b/code/src/vs/workbench/contrib/notebook/browser/media/notebookFolding.css index 7433c9a7eb2..88bfc96b8f6 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/media/notebookFolding.css +++ b/code/src/vs/workbench/contrib/notebook/browser/media/notebookFolding.css @@ -46,6 +46,8 @@ .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folded-hint { position: absolute; user-select: none; + display: flex; + align-items: center; } .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folded-hint-label { @@ -55,6 +57,22 @@ opacity: 0.7; } +.monaco-workbench .notebookOverlay > .cell-list-container .folded-cell-run-section-button { + position: relative; + left: 0px; + padding: 2px; + border-radius: 5px; + margin-right: 4px; + height: 16px; + width: 16px; + z-index: var(--z-index-notebook-cell-expand-part-button); +} + +.monaco-workbench .notebookOverlay > .cell-list-container .folded-cell-run-section-button:hover { + background-color: var(--vscode-editorStickyScrollHover-background); + cursor: pointer; +} + .monaco-workbench .notebookOverlay .cell-editor-container .monaco-editor .margin-view-overlays .codicon-folding-expanded, .monaco-workbench .notebookOverlay .cell-editor-container .monaco-editor .margin-view-overlays .codicon-folding-collapsed { margin-left: 0; diff --git a/code/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css b/code/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css index 6e71e660f87..677f9c89ea7 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css +++ b/code/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css @@ -43,3 +43,19 @@ /* Don't show markers inline with breadcrumbs */ display: none; } + +.monaco-list-row .notebook-outline-element .action-menu { + display: none; +} + +.monaco-list-row.focused.selected .notebook-outline-element .action-menu { + display: flex; +} + +.monaco-list-row:hover .notebook-outline-element .action-menu { + display: flex; +} + +.monaco-list-row .notebook-outline-element.notebook-outline-toolbar-dropdown-active .action-menu { + display: flex; +} diff --git a/code/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css b/code/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css index 28ea1557a52..cb19b73e48b 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css +++ b/code/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css @@ -70,6 +70,10 @@ display: inline-flex; } +.monaco-workbench .notebook-action-view-item-unified .monaco-dropdown { + pointer-events: none; +} + .monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .notebook-label { background-size: 16px; padding: 0px 5px 0px 2px; diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index cf5cf217587..4ba6035175d 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -62,6 +62,7 @@ import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook import 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; import 'vs/workbench/contrib/notebook/browser/controller/executeActions'; +import 'vs/workbench/contrib/notebook/browser/controller/sectionActions'; import 'vs/workbench/contrib/notebook/browser/controller/layoutActions'; import 'vs/workbench/contrib/notebook/browser/controller/editActions'; import 'vs/workbench/contrib/notebook/browser/controller/cellOutputActions'; @@ -1090,7 +1091,7 @@ configurationRegistry.registerConfiguration({ default: 'auto' }, [NotebookSetting.cellChat]: { - markdownDescription: nls.localize('notebook.cellChat', "Enable experimental cell chat for notebooks."), + markdownDescription: nls.localize('notebook.cellChat', "Enable experimental floating chat widget in notebooks."), type: 'boolean', default: false }, diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 4f6205e00cc..a502b32b708 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -449,6 +449,7 @@ export interface INotebookViewModel { layoutInfo: NotebookLayoutInfo | null; onDidChangeViewCells: Event; onDidChangeSelection: Event; + onDidFoldingStateChanged: Event; getNearestVisibleCellIndexUpwards(index: number): number; getTrackedRange(id: string): ICellRange | null; setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null; @@ -581,6 +582,11 @@ export interface INotebookEditor { * Copy the image in the specific cell output to the clipboard */ copyOutputImage(cellOutput: ICellOutputViewModel): Promise; + /** + * Select the contents of the first focused output of the cell. + * Implementation of Ctrl+A for an output item. + */ + selectOutputContent(cell: ICellViewModel): void; readonly onDidReceiveMessage: Event; @@ -629,6 +635,11 @@ export interface INotebookEditor { */ revealInCenterIfOutsideViewport(cell: ICellViewModel): Promise; + /** + * Reveal the first line of the cell into the view if the cell is outside of the viewport. + */ + revealFirstLineIfOutsideViewport(cell: ICellViewModel): Promise; + /** * Reveal a line in notebook cell into viewport with minimal scrolling. */ diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 0a4cc62f4ed..b4802da503f 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -24,7 +24,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Selection } from 'vs/editor/common/core/selection'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { DEFAULT_EDITOR_ASSOCIATION, EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, EditorResourceAccessor, IEditorMemento, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, createEditorOpenError, createTooLargeFileError, isEditorOpenError } from 'vs/workbench/common/editor'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, EditorResourceAccessor, IEditorMemento, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling, createEditorOpenError, createTooLargeFileError, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { INotebookEditorOptions, INotebookEditorPane, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -51,7 +51,7 @@ import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewI const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; -export class NotebookEditor extends EditorPane implements INotebookEditorPane { +export class NotebookEditor extends EditorPane implements INotebookEditorPane, IEditorPaneWithScrolling { static readonly ID: string = NOTEBOOK_EDITOR_ID; private readonly _editorMemento: IEditorMemento; @@ -75,7 +75,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { private readonly _onDidChangeSelection = this._register(new Emitter()); readonly onDidChangeSelection = this._onDidChangeSelection.event; + protected readonly _onDidChangeScroll = this._register(new Emitter()); + readonly onDidChangeScroll = this._onDidChangeScroll.event; + constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -94,7 +98,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { @INotebookEditorWorkerService private readonly _notebookEditorWorkerService: INotebookEditorWorkerService, @IPreferencesService private readonly _preferencesService: IPreferencesService ) { - super(NotebookEditor.ID, telemetryService, themeService, storageService); + super(NotebookEditor.ID, group, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); this._register(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._onDidChangeFileSystemProvider(e.scheme))); @@ -150,24 +154,22 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { return this._widget.value; } - override setVisible(visible: boolean, group?: IEditorGroup | undefined): void { - super.setVisible(visible, group); + override setVisible(visible: boolean): void { + super.setVisible(visible); if (!visible) { this._widget.value?.onWillHide(); } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - this._groupListener.add(group.onDidModelChange(() => { - if (this._editorGroupService.activeGroup !== group) { - this._widget?.value?.updateEditorFocus(); - } - })); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.clear(); + this._groupListener.add(this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); + this._groupListener.add(this.group.onDidModelChange(() => { + if (this._editorGroupService.activeGroup !== this.group) { + this._widget?.value?.updateEditorFocus(); + } + })); if (!visible) { this._saveEditorViewState(this.input); @@ -203,7 +205,6 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { const perf = new NotebookPerfMarks(); perf.mark('startTime'); - const group = this.group!; this._inputListener.value = input.onDidChangeCapabilities(() => this._onDidChangeInputCapabilities(input)); @@ -213,7 +214,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { // we need to hide it before getting a new widget this._widget.value?.onWillHide(); - this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input, undefined, this._pagePosition?.dimension, DOM.getWindowById(group.windowId, true).window); + this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, input, undefined, this._pagePosition?.dimension, this.window); if (this._rootElement && this._widget.value!.getDomNode()) { this._rootElement.setAttribute('aria-flowto', this._widget.value!.getDomNode().id || ''); @@ -319,9 +320,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._widgetDisposableStore.add(this._widget.value.onDidBlurWidget(() => this._onDidBlurWidget.fire())); this._widgetDisposableStore.add(this._editorGroupService.createEditorDropTarget(this._widget.value.getDomNode(), { - containsGroup: (group) => this.group?.id === group.id + containsGroup: (group) => this.group.id === group.id })); + this._widgetDisposableStore.add(this._widget.value.onDidScroll(() => { this._onDidChangeScroll.fire(); })); + perf.mark('editorLoaded'); fileOpenMonitor.cancel(); @@ -338,7 +341,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } // Handle case where a file is too large to open without confirmation - if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (e instanceof TooLargeFileOperationError) { message = localize('notebookTooLargeForHeapErrorWithSize', "The notebook is not displayed in the notebook editor because it is very large ({0}).", ByteSize.formatSize(e.size)); @@ -510,9 +513,29 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { return undefined; } + getScrollPosition(): IEditorPaneScrollPosition { + const widget = this.getControl(); + if (!widget) { + throw new Error('Notebook widget has not yet been initialized'); + } + + return { + scrollTop: widget.scrollTop, + scrollLeft: 0, + }; + } + + setScrollPosition(scrollPosition: IEditorPaneScrollPosition): void { + const editor = this.getControl(); + if (!editor) { + throw new Error('Control has not yet been initialized'); + } + + editor.setScrollTop(scrollPosition.scrollTop); + } private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._widget.value && input instanceof NotebookEditorInput) { + if (this._widget.value && input instanceof NotebookEditorInput) { if (this._widget.value.isDisposed) { return; } @@ -523,10 +546,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } private _loadNotebookEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined { - let result: INotebookEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.resource); if (result) { return result; } @@ -545,11 +565,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._rootElement.classList.toggle('narrow-width', dimension.width < 600); this._pagePosition = { dimension, position }; - if (!this._widget.value || !(this._input instanceof NotebookEditorInput)) { + if (!this._widget.value || !(this.input instanceof NotebookEditorInput)) { return; } - if (this._input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { + if (this.input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { // input and widget mismatch // this happens when // 1. open document A, pin the document diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index cdffe911b5d..7c9258a30e0 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -392,7 +392,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } })); - this._register(editorGroupsService.activePart.onDidScroll(e => { + const container = creationOptions.codeWindow ? this.layoutService.getContainer(creationOptions.codeWindow) : this.layoutService.mainContainer; + this._register(editorGroupsService.getPart(container).onDidScroll(e => { if (!this._shadowElement || !this._isVisible) { return; } @@ -409,11 +410,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._overlayContainer.classList.add('notebook-editor'); this._overlayContainer.style.visibility = 'hidden'; - if (creationOptions.codeWindow) { - this.layoutService.getContainer(creationOptions.codeWindow).appendChild(this._overlayContainer); - } else { - this.layoutService.mainContainer.appendChild(this._overlayContainer); - } + container.appendChild(this._overlayContainer); this._createBody(this._overlayContainer); this._generateFontInfo(); @@ -1025,9 +1022,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); this._register(this._list.onDidScroll((e) => { - this._onDidScroll.fire(); - if (e.scrollTop !== e.oldScrollTop) { + this._onDidScroll.fire(); this.clearActiveCellWidgets(); } })); @@ -1980,6 +1976,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } + selectOutputContent(cell: ICellViewModel) { + this._webview?.selectOutputContents(cell); + } + onWillHide() { this._isVisible = false; this._editorFocus.set(false); @@ -2122,8 +2122,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD await this._list.revealCell(cell, CellRevealType.CenterIfOutsideViewport); } - revealFirstLineIfOutsideViewport(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealType.FirstLineIfOutsideViewport); + async revealFirstLineIfOutsideViewport(cell: ICellViewModel) { + await this._list.revealCell(cell, CellRevealType.FirstLineIfOutsideViewport); } async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { @@ -2446,7 +2446,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._cursorNavMode.set(true); await this.revealInView(cell); } else if (options?.revealBehavior === ScrollToRevealBehavior.firstLine) { - this.revealFirstLineIfOutsideViewport(cell); + await this.revealFirstLineIfOutsideViewport(cell); } else if (options?.revealBehavior === ScrollToRevealBehavior.fullCell) { await this.revealInView(cell); } else { diff --git a/code/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts b/code/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts index dbc9e88e9a4..52157f4ae29 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts @@ -527,6 +527,7 @@ class CellExecution extends Disposable implements INotebookCellExecution { lastRunSuccess: completionData.lastRunSuccess, runStartTime: this._didPause ? null : cellModel.internalMetadata.runStartTime, runEndTime: this._didPause ? null : completionData.runEndTime, + error: completionData.error } }; this._applyExecutionEdits([edit]); diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts index 35007bedeb8..0c859ef8476 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts @@ -3,18 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import * as DOM from 'vs/base/browser/dom'; -import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import * as types from 'vs/base/common/types'; +import { EventType as TouchEventType } from 'vs/base/browser/touch'; import { IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IActionProvider } from 'vs/base/browser/ui/dropdown/dropdown'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { IAction } from 'vs/base/common/actions'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; export class CodiconActionViewItem extends MenuEntryActionViewItem { @@ -46,6 +49,7 @@ export class ActionViewWithLabel extends MenuEntryActionViewItem { export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { private _actionLabel?: HTMLAnchorElement; private _hover?: ICustomHover; + private _primaryAction: IAction | undefined; constructor( action: SubmenuItemAction, @@ -63,18 +67,30 @@ export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { override render(container: HTMLElement): void { super.render(container); container.classList.add('notebook-action-view-item'); + container.classList.add('notebook-action-view-item-unified'); this._actionLabel = document.createElement('a'); container.appendChild(this._actionLabel); this._hover = this._register(setupCustomHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._actionLabel, '')); this.updateLabel(); + + for (const event of [DOM.EventType.CLICK, DOM.EventType.MOUSE_DOWN, TouchEventType.Tap]) { + this._register(DOM.addDisposableListener(container, event, e => this.onClick(e, true))); + } + } + + override onClick(event: DOM.EventLike, preserveFocus = false): void { + DOM.EventHelper.stop(event, true); + const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : { preserveFocus } : this._context; + this.actionRunner.run(this._primaryAction ?? this._action, context); } protected override updateLabel() { const actions = this.subActionProvider.getActions(); if (this._actionLabel) { const primaryAction = actions[0]; + this._primaryAction = primaryAction; if (primaryAction && primaryAction instanceof MenuItemAction) { const element = this.element; diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index bf0fe703109..1c42939cab4 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -141,7 +141,7 @@ export class CellComments extends CellContentPart { if (this.notebookEditor.hasModel()) { const commentInfos = coalesce(await this.commentService.getNotebookComments(element.uri)); if (commentInfos.length && commentInfos[0].threads.length) { - return { owner: commentInfos[0].owner, thread: commentInfos[0].threads[0] }; + return { owner: commentInfos[0].uniqueOwner, thread: commentInfos[0].threads[0] }; } } diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index eb2c45f2f8a..7bc1ba28b3a 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -6,13 +6,14 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export class CellContextKeyPart extends CellContentPart { @@ -45,6 +46,7 @@ export class CellContextKeyManager extends Disposable { private cellOutputCollapsed!: IContextKey; private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; private cellResource!: IContextKey; + private cellGeneratedByChat!: IContextKey; private markdownEditMode!: IContextKey; @@ -70,6 +72,7 @@ export class CellContextKeyManager extends Disposable { this.cellContentCollapsed = NOTEBOOK_CELL_INPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); + this.cellGeneratedByChat = NOTEBOOK_CELL_GENERATED_BY_CHAT.bindTo(this._contextKeyService); this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService); if (element) { @@ -112,10 +115,21 @@ export class CellContextKeyManager extends Disposable { this.updateForEditState(); this.updateForCollapseState(); this.updateForOutputs(); + this.updateForChat(); this.cellLineNumbers.set(this.element!.lineNumbers); this.cellResource.set(this.element!.uri.toString()); }); + + const chatController = NotebookChatController.get(this.notebookEditor); + + if (chatController) { + this.elementDisposables.add(chatController.onDidChangePromptCache(e => { + if (e.cell.toString() === this.element!.uri.toString()) { + this.updateForChat(); + } + })); + } } private onDidChangeState(e: CellViewModelStateChangeEvent) { @@ -216,4 +230,15 @@ export class CellContextKeyManager extends Disposable { this.cellHasOutputs.set(false); } } + + private updateForChat() { + const chatController = NotebookChatController.get(this.notebookEditor); + + if (!chatController || !this.element) { + this.cellGeneratedByChat.set(false); + return; + } + + this.cellGeneratedByChat.set(chatController.isCellGeneratedByChat(this.element)); + } } diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index 52e87ad8086..88c4252fd82 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -27,8 +27,8 @@ import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cell import { ClickTargetType, IClickTarget } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellWidgets'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ITooltipMarkdownString, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { ITooltipMarkdownString, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; @@ -294,7 +294,7 @@ class CellStatusBarItem extends Disposable { this._itemDisposables.clear(); if (!this._currentItem || this._currentItem.text !== item.text) { - new SimpleIconLabel(this.container).text = item.text.replace(/\n/g, ' '); + this._itemDisposables.add(new SimpleIconLabel(this.container)).text = item.text.replace(/\n/g, ' '); } const resolveColor = (color: ThemeColor | string) => { diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts index f10530616f1..250cb9824ec 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts @@ -22,8 +22,8 @@ import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/vie import { CellOverlayPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { registerCellToolbarStickyScroll } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; export class BetweenCellToolbar extends CellOverlayPart { private _betweenCellToolbar: ToolBar | undefined; @@ -167,7 +167,7 @@ export class CellTitleToolbarPart extends CellOverlayPart { if (this._view) { return this._view; } - const hoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + const hoverDelegate = this._register(createInstantHoverDelegate()); const toolbar = this._register(this.instantiationService.createInstance(WorkbenchToolBar, this.toolbarContainer, { actionViewItemProvider: (action, options) => { return createActionViewItem(this.instantiationService, action, options); diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts index 2fe72e05af8..211e85e9a62 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts @@ -11,12 +11,21 @@ import { FoldingController } from 'vs/workbench/contrib/notebook/browser/control import { CellEditState, CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { executingStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; export class FoldedCellHint extends CellContentPart { + private readonly _runButtonListener = this._register(new MutableDisposable()); + private readonly _cellExecutionListener = this._register(new MutableDisposable()); + constructor( private readonly _notebookEditor: INotebookEditor, private readonly _container: HTMLElement, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService ) { super(); } @@ -27,20 +36,27 @@ export class FoldedCellHint extends CellContentPart { private update(element: MarkupCellViewModel) { if (!this._notebookEditor.hasModel()) { + this._cellExecutionListener.clear(); + this._runButtonListener.clear(); return; } if (element.isInputCollapsed || element.getEditState() === CellEditState.Editing) { + this._cellExecutionListener.clear(); + this._runButtonListener.clear(); DOM.hide(this._container); } else if (element.foldingState === CellFoldingState.Collapsed) { const idx = this._notebookEditor.getViewModel().getCellIndex(element); const length = this._notebookEditor.getViewModel().getFoldedLength(idx); - DOM.reset(this._container, this.getHiddenCellsLabel(length), this.getHiddenCellHintButton(element)); + + DOM.reset(this._container, this.getRunFoldedSectionButton({ start: idx, end: idx + length }), this.getHiddenCellsLabel(length), this.getHiddenCellHintButton(element)); DOM.show(this._container); const foldHintTop = element.layoutInfo.previewHeight; this._container.style.top = `${foldHintTop}px`; } else { + this._cellExecutionListener.clear(); + this._runButtonListener.clear(); DOM.hide(this._container); } } @@ -67,6 +83,40 @@ export class FoldedCellHint extends CellContentPart { return expandIcon; } + private getRunFoldedSectionButton(range: ICellRange): HTMLElement { + const runAllContainer = DOM.$('span.folded-cell-run-section-button'); + const cells = this._notebookEditor.getCellsInRange(range); + + const isRunning = cells.some(cell => { + const cellExecution = this._notebookExecutionStateService.getCellExecution(cell.uri); + return cellExecution && cellExecution.state === NotebookCellExecutionState.Executing; + }); + + const runAllIcon = isRunning ? + ThemeIcon.modify(executingStateIcon, 'spin') : + Codicon.play; + runAllContainer.classList.add(...ThemeIcon.asClassNameArray(runAllIcon)); + + this._runButtonListener.value = DOM.addDisposableListener(runAllContainer, DOM.EventType.CLICK, () => { + this._notebookEditor.executeNotebookCells(cells); + }); + + this._cellExecutionListener.value = this._notebookExecutionStateService.onDidChangeExecution(() => { + const isRunning = cells.some(cell => { + const cellExecution = this._notebookExecutionStateService.getCellExecution(cell.uri); + return cellExecution && cellExecution.state === NotebookCellExecutionState.Executing; + }); + + const runAllIcon = isRunning ? + ThemeIcon.modify(executingStateIcon, 'spin') : + Codicon.play; + runAllContainer.className = ''; + runAllContainer.classList.add('folded-cell-run-section-button', ...ThemeIcon.asClassNameArray(runAllIcon)); + }); + + return runAllContainer; + } + override updateInternalLayoutNow(element: MarkupCellViewModel) { this.update(element); } diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index bd47eb879e9..377857dd4ce 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -925,8 +925,12 @@ export class NotebookCellList extends WorkbenchList implements ID break; } - // wait for the editor to be created only if the cell is in editing mode (meaning it has an editor and will focus the editor) - if (cell.getEditState() === CellEditState.Editing && !cell.editorAttached) { + if (( + // wait for the editor to be created if the cell is in editing mode + cell.getEditState() === CellEditState.Editing + // wait for the editor to be created if we are revealing the first line of the cell + || revealType === CellRevealType.FirstLineIfOutsideViewport + ) && !cell.editorAttached) { return getEditorAttachedPromise(cell); } diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index e7f7d018a80..399934d7c49 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -295,8 +295,20 @@ export class NotebookCellListView extends ListView { } removeWhitespace(id: string): void { - this.notebookRangeMap.removeWhitespace(id); - this.eventuallyUpdateScrollDimensions(); + const scrollTop = this.scrollTop; + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const currentPosition = this.notebookRangeMap.getWhitespacePosition(id); + + if (currentPosition > scrollTop) { + this.notebookRangeMap.removeWhitespace(id); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); + } else { + this.notebookRangeMap.removeWhitespace(id); + this.eventuallyUpdateScrollDimensions(); + } + } getWhitespacePosition(id: string): number { diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index dd421641fb8..a005797d87e 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -391,6 +391,14 @@ export class BackLayerWebView extends Themable { background-color: var(--theme-notebook-symbol-highlight-background); } + #container .nb-symbolHighlight .output_container .output { + background-color: var(--theme-notebook-symbol-highlight-background); + } + + #container .nb-chatGenerationHighlight .output_container .output { + background-color: var(--vscode-notebook-selectedCellBackground); + } + #container > div.nb-cellDeleted .output_container { background-color: var(--theme-notebook-diff-removed-background); } @@ -513,10 +521,10 @@ export class BackLayerWebView extends Themable { return !!this.webview; } - createWebview(codeWindow: CodeWindow): Promise { + createWebview(targetWindow: CodeWindow): Promise { const baseUrl = this.asWebviewUri(this.getNotebookBaseUri(), undefined); const htmlContent = this.generateContent(baseUrl.toString()); - return this._initialize(htmlContent, codeWindow); + return this._initialize(htmlContent, targetWindow); } private getNotebookBaseUri() { @@ -551,16 +559,16 @@ export class BackLayerWebView extends Themable { ]; } - private _initialize(content: string, codeWindow: CodeWindow): Promise { + private _initialize(content: string, targetWindow: CodeWindow): Promise { if (!getWindow(this.element).document.body.contains(this.element)) { throw new Error('Element is already detached from the DOM tree'); } - this.webview = this._createInset(this.webviewService, content, codeWindow); - this.webview.mountTo(this.element); + this.webview = this._createInset(this.webviewService, content); + this.webview.mountTo(this.element, targetWindow); this._register(this.webview); - this._register(new WebviewWindowDragMonitor(() => this.webview)); + this._register(new WebviewWindowDragMonitor(targetWindow, () => this.webview)); const initializePromise = new DeferredPromise(); @@ -1123,7 +1131,7 @@ export class BackLayerWebView extends Themable { await this.openerService.open(newFileUri); } - private _createInset(webviewService: IWebviewService, content: string, codeWindow: CodeWindow) { + private _createInset(webviewService: IWebviewService, content: string) { this.localResourceRootsCache = this._getResourceRootsCache(); const webview = webviewService.createWebviewElement({ origin: BackLayerWebView.getOriginStore(this.storageService).getOrigin(this.notebookViewType, undefined), @@ -1139,8 +1147,7 @@ export class BackLayerWebView extends Themable { localResourceRoots: this.localResourceRootsCache, }, extension: undefined, - providedViewType: 'notebook.output', - codeWindow: codeWindow + providedViewType: 'notebook.output' }); webview.setHtml(content); @@ -1674,6 +1681,18 @@ export class BackLayerWebView extends Themable { this.webview?.focus(); } + selectOutputContents(cell: ICellViewModel) { + if (this._disposed) { + return; + } + const output = cell.outputsViewModels.find(o => o.model.outputId === cell.focusedOutputId); + const outputId = output ? this.insetMapping.get(output)?.outputId : undefined; + this._sendMessageToWebview({ + type: 'select-output-contents', + cellOrOutputId: outputId || cell.id + }); + } + focusOutput(cellOrOutputId: string, alternateId: string | undefined, viewFocused: boolean) { if (this._disposed) { return; diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 25a1310af43..9f6f5b8dac5 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -49,6 +49,7 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; const $ = DOM.$; @@ -109,6 +110,8 @@ abstract class AbstractCellRenderer { export class MarkupCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'markdown_cell'; + private _notebookExecutionStateService: INotebookExecutionStateService; + constructor( notebookEditor: INotebookEditorDelegate, dndController: CellDragAndDropController, @@ -120,8 +123,10 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen @IMenuService menuService: IMenuService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, + @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService ) { super(instantiationService, notebookEditor, contextMenuService, menuService, configurationService, keybindingService, notificationService, contextKeyServiceProvider, 'markdown', dndController); + this._notebookExecutionStateService = notebookExecutionStateService; } get templateId() { @@ -169,7 +174,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen templateDisposables.add(scopedInstaService.createInstance(CellChatPart, this.notebookEditor, cellChatPart)), templateDisposables.add(scopedInstaService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart, undefined)), templateDisposables.add(new CellFocusIndicator(this.notebookEditor, titleToolbar, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom)), - templateDisposables.add(new FoldedCellHint(this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))), + templateDisposables.add(new FoldedCellHint(this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')), this._notebookExecutionStateService)), templateDisposables.add(new CellDecorations(rootContainer, decorationContainer)), templateDisposables.add(scopedInstaService.createInstance(CellComments, this.notebookEditor, cellCommentPartContainer)), templateDisposables.add(new CollapsedCellInput(this.notebookEditor, cellInputCollapsedContainer)), diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index b46964be307..12a4f4f7e01 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -168,23 +168,6 @@ export interface IClearMessage { readonly type: 'clear'; } -export interface IOutputRequestMetadata { - /** - * Additional attributes of a cell metadata. - */ - readonly custom?: { readonly [key: string]: unknown }; -} - -export interface IOutputRequestDto { - /** - * { mime_type: value } - */ - readonly data: { readonly [key: string]: unknown }; - - readonly metadata?: IOutputRequestMetadata; - readonly outputId: string; -} - export interface OutputItemEntry { readonly mime: string; readonly valueBytes: Uint8Array; @@ -476,6 +459,11 @@ export interface IReturnOutputItemMessage { readonly output: OutputItemEntry | undefined; } +export interface ISelectOutputItemMessage { + readonly type: 'select-output-contents'; + readonly cellOrOutputId: string; +} + export interface ILogRendererDebugMessage extends BaseToWebviewMessage { readonly type: 'logRendererDebugMessage'; readonly message: string; @@ -555,7 +543,8 @@ export type ToWebviewMessage = IClearMessage | IFindHighlightCurrentMessage | IFindUnHighlightCurrentMessage | IFindStopMessage | - IReturnOutputItemMessage; + IReturnOutputItemMessage | + ISelectOutputItemMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index c08eb70c296..5781cdb6a3f 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -159,10 +159,36 @@ async function webviewPreloads(ctx: PreloadContext) { } }; }; + function getOutputContainer(event: FocusEvent | MouseEvent) { + for (const node of event.composedPath()) { + if (node instanceof HTMLElement && node.classList.contains('output')) { + return { + id: node.id + }; + } + } + return; + } + let lastFocusedOutput: { id: string } | undefined = undefined; + const handleOutputFocusOut = (event: FocusEvent) => { + const outputFocus = event && getOutputContainer(event); + if (!outputFocus) { + return; + } + // Possible we're tabbing through the elements of the same output. + // Lets see if focus is set back to the same output. + lastFocusedOutput = undefined; + setTimeout(() => { + if (lastFocusedOutput?.id === outputFocus.id) { + return; + } + postNotebookMessage('outputBlur', outputFocus); + }, 0); + }; // check if an input element is focused within the output element - const checkOutputInputFocus = () => { - + const checkOutputInputFocus = (e: FocusEvent) => { + lastFocusedOutput = getOutputContainer(e); const activeElement = window.document.activeElement; if (!activeElement) { return; @@ -182,16 +208,7 @@ async function webviewPreloads(ctx: PreloadContext) { return; } - let outputFocus: { id: string } | undefined = undefined; - for (const node of event.composedPath()) { - if (node instanceof HTMLElement && node.classList.contains('output')) { - outputFocus = { - id: node.id - }; - break; - } - } - + const outputFocus = lastFocusedOutput = getOutputContainer(event); for (const node of event.composedPath()) { if (node instanceof HTMLAnchorElement && node.href) { if (node.href.startsWith('blob:')) { @@ -253,6 +270,53 @@ async function webviewPreloads(ctx: PreloadContext) { postNotebookMessage('outputFocus', outputFocus); } }; + const selectOutputContents = (cellOrOutputId: string) => { + const selection = window.getSelection(); + if (!selection) { + return; + } + const cellOutputContainer = window.document.getElementById(cellOrOutputId); + if (!cellOutputContainer) { + return; + } + selection.removeAllRanges(); + const range = document.createRange(); + range.selectNode(cellOutputContainer); + selection.addRange(range); + + }; + + const onPageUpDownSelectionHandler = (e: KeyboardEvent) => { + if (!lastFocusedOutput?.id || !e.shiftKey) { + return; + } + // We want to handle just `Shift + PageUp/PageDown` & `Shift + Cmd + ArrowUp/ArrowDown` (for mac) + if (!(e.code === 'PageUp' || e.code === 'PageDown') && !(e.metaKey && (e.code === 'ArrowDown' || e.code === 'ArrowUp'))) { + return; + } + const outputContainer = window.document.getElementById(lastFocusedOutput.id); + const selection = window.getSelection(); + if (!outputContainer || !selection?.anchorNode) { + return; + } + + // These should change the scroll position, not adjust the selected cell in the notebook + e.stopPropagation(); // We don't want the notebook to handle this. + e.preventDefault(); // We will handle selection. + + const { anchorNode, anchorOffset } = selection; + const range = document.createRange(); + if (e.code === 'PageDown' || e.code === 'ArrowDown') { + range.setStart(anchorNode, anchorOffset); + range.setEnd(outputContainer, 1); + } + else { + range.setStart(outputContainer, 0); + range.setEnd(anchorNode, anchorOffset); + } + selection.removeAllRanges(); + selection.addRange(range); + }; const handleDataUrl = async (data: string | ArrayBuffer | null, downloadName: string) => { postNotebookMessage('clicked-data-url', { @@ -277,6 +341,8 @@ async function webviewPreloads(ctx: PreloadContext) { window.document.body.addEventListener('click', handleInnerClick); window.document.body.addEventListener('focusin', checkOutputInputFocus); + window.document.body.addEventListener('focusout', handleOutputFocusOut); + window.document.body.addEventListener('keydown', onPageUpDownSelectionHandler); interface RendererContext extends rendererApi.RendererContext { readonly onDidChangeSettings: Event; @@ -455,15 +521,30 @@ async function webviewPreloads(ctx: PreloadContext) { } }; - function scrollWillGoToParent(event: WheelEvent) { + let scrollTimeout: any /* NodeJS.Timeout */ | undefined; + let scrolledElement: Element | undefined; + function flagRecentlyScrolled(node: Element) { + scrolledElement = node; + node.setAttribute('recentlyScrolled', 'true'); + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300); + } + + function eventTargetShouldHandleScroll(event: WheelEvent) { for (let node = event.target as Node | null; node; node = node.parentNode) { if (!(node instanceof Element) || node.id === 'container' || node.classList.contains('cell_container') || node.classList.contains('markup') || node.classList.contains('output_container')) { return false; } + if (node.hasAttribute('recentlyScrolled') && scrolledElement === node) { + flagRecentlyScrolled(node); + return true; + } + // scroll up if (event.deltaY < 0 && node.scrollTop > 0) { // there is still some content to scroll + flagRecentlyScrolled(node); return true; } @@ -481,6 +562,7 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } + flagRecentlyScrolled(node); return true; } } @@ -489,7 +571,7 @@ async function webviewPreloads(ctx: PreloadContext) { } const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => { - if (event.defaultPrevented || scrollWillGoToParent(event)) { + if (event.defaultPrevented || eventTargetShouldHandleScroll(event)) { return; } postNotebookMessage('did-scroll-wheel', { @@ -1575,6 +1657,9 @@ async function webviewPreloads(ctx: PreloadContext) { case 'focus-output': focusFirstFocusableOrContainerInOutput(event.data.cellOrOutputId, event.data.alternateId); break; + case 'select-output-contents': + selectOutputContents(event.data.cellOrOutputId); + break; case 'decorations': { let outputContainer = window.document.getElementById(event.data.cellId); if (!outputContainer) { diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 548a5a7c043..dd136ac471b 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, IReference, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -19,11 +19,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IWordWrapTransientState, readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { CellEditState, CellFocusMode, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; export abstract class BaseCellViewModel extends Disposable { @@ -103,6 +103,7 @@ export abstract class BaseCellViewModel extends Disposable { private _editorViewStates: editorCommon.ICodeEditorViewState | null = null; private _editorTransientState: IWordWrapTransientState | null = null; private _resolvedCellDecorations = new Map(); + private _textModelRefChangeDisposable = this._register(new MutableDisposable()); private readonly _cellDecorationsChanged = this._register(new Emitter<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>()); onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }> = this._cellDecorationsChanged.event; @@ -299,6 +300,7 @@ export abstract class BaseCellViewModel extends Disposable { this._textModelRef.dispose(); this._textModelRef = undefined; } + this._textModelRefChangeDisposable.clear(); } getText(): string { @@ -618,8 +620,7 @@ export abstract class BaseCellViewModel extends Disposable { if (!this._textModelRef) { throw new Error(`Cannot resolve text model for ${this.uri}`); } - - this._register(this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent())); + this._textModelRefChangeDisposable.value = this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent()); } return this.textModel!; diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts index bdb16299dc1..c57adfbdbdf 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts @@ -70,7 +70,7 @@ export class NotebookCellOutlineProvider { ); this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.outline.showCodeCells')) { + if (e.affectsConfiguration('notebook.outline.showCodeCells') || e.affectsConfiguration('notebook.outline.showNonHeaderMarkdownCells')) { this._recomputeState(); } })); @@ -142,6 +142,8 @@ export class NotebookCellOutlineProvider { includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); } + const showNonHeaderMarkdownCells = this._configurationService.getValue('notebook.outline.showNonHeaderMarkdownCells'); + const notebookCells = notebookEditorWidget.getViewModel().viewCells.filter((cell) => cell.cellKind === CellKind.Markup || includeCodeCells); const entries: OutlineEntry[] = []; @@ -162,6 +164,11 @@ export class NotebookCellOutlineProvider { for (let i = 1; i < entries.length; i++) { const entry = entries[i]; + if (!showNonHeaderMarkdownCells && entry.cell.cellKind === CellKind.Markup && entry.level === 7) { + // skip plain text markdown cells + continue; + } + while (true) { const len = parentStack.length; if (len === 0) { diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 6444aa95c45..1e17428ab58 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -177,6 +177,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private readonly _instanceId: string; public readonly id: string; private _foldingRanges: FoldingRegions | null = null; + private _onDidFoldingStateChanged = new Emitter(); + onDidFoldingStateChanged: Event = this._onDidFoldingStateChanged.event; private _hiddenRanges: ICellRange[] = []; private _focused: boolean = true; @@ -470,6 +472,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD if (updateHiddenAreas || k < this._hiddenRanges.length) { this._hiddenRanges = newHiddenAreas; + this._onDidFoldingStateChanged.fire(); } this._viewCells.forEach(cell => { diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index a02e1bb2010..1542d675165 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -3,17 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { EventType as TouchEventType } from 'vs/base/browser/touch'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; @@ -26,35 +21,7 @@ import { foldingCollapsedIcon, foldingExpandedIcon } from 'vs/editor/contrib/fol import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { FoldingController } from 'vs/workbench/contrib/notebook/browser/controller/foldingController'; import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; - -export class ToggleNotebookStickyScroll extends Action2 { - - constructor() { - super({ - id: 'notebook.action.toggleNotebookStickyScroll', - title: { - ...localize2('toggleStickyScroll', "Toggle Notebook Sticky Scroll"), - mnemonicTitle: localize({ key: 'mitoggleStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), - }, - category: Categories.View, - toggled: { - condition: ContextKeyExpr.equals('config.notebook.stickyScroll.enabled', true), - title: localize('notebookStickyScroll', "Notebook Sticky Scroll"), - mnemonicTitle: localize({ key: 'miNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Notebook Sticky Scroll"), - }, - menu: [ - { id: MenuId.CommandPalette }, - { id: MenuId.NotebookStickyScrollContext } - ] - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - const newValue = !configurationService.getValue('notebook.stickyScroll.enabled'); - return configurationService.updateValue('notebook.stickyScroll.enabled', newValue); - } -} +import { NotebookSectionArgs } from 'vs/workbench/contrib/notebook/browser/controller/sectionActions'; export class NotebookStickyLine extends Disposable { constructor( @@ -78,14 +45,6 @@ export class NotebookStickyLine extends Disposable { } })); - // folding icon hovers - // this._register(DOM.addDisposableListener(this.element, DOM.EventType.MOUSE_OVER, () => { - // this.foldingIcon.setVisible(true); - // })); - // this._register(DOM.addDisposableListener(this.element, DOM.EventType.MOUSE_OUT, () => { - // this.foldingIcon.setVisible(false); - // })); - } private toggleFoldRange(currentState: CellFoldingState) { @@ -95,7 +54,7 @@ export class NotebookStickyLine extends Disposable { const headerLevel = this.entry.level; const newFoldingState = (currentState === CellFoldingState.Collapsed) ? CellFoldingState.Expanded : CellFoldingState.Collapsed; - foldingController.setFoldingStateUp(index, newFoldingState, headerLevel); + foldingController.setFoldingStateDown(index, newFoldingState, headerLevel); this.focusCell(); } @@ -140,12 +99,10 @@ class StickyFoldingIcon { export class NotebookStickyScroll extends Disposable { private readonly _disposables = new DisposableStore(); private currentStickyLines = new Map(); - private filteredOutlineEntries: OutlineEntry[] = []; private readonly _onDidChangeNotebookStickyScroll = this._register(new Emitter()); readonly onDidChangeNotebookStickyScroll: Event = this._onDidChangeNotebookStickyScroll.event; - getDomNode(): HTMLElement { return this.domNode; } @@ -205,9 +162,22 @@ export class NotebookStickyScroll extends Disposable { private onContextMenu(e: MouseEvent) { const event = new StandardMouseEvent(DOM.getWindow(this.domNode), e); + + const selectedElement = event.target.parentElement; + const selectedOutlineEntry = Array.from(this.currentStickyLines.values()).find(entry => entry.line.element.contains(selectedElement))?.line.entry; + if (!selectedOutlineEntry) { + return; + } + + const args: NotebookSectionArgs = { + outlineEntry: selectedOutlineEntry, + notebookEditor: this.notebookEditor, + }; + this._contextMenuService.showContextMenu({ menuId: MenuId.NotebookStickyScrollContext, getAnchor: () => event, + menuActionOptions: { shouldForwardArgs: true, arg: args }, }); } @@ -223,18 +193,16 @@ export class NotebookStickyScroll extends Disposable { this.updateDisplay(); } } else if (e.stickyScrollMode && this.notebookEditor.notebookOptions.getDisplayOptions().stickyScrollEnabled) { - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.filteredOutlineEntries, this.getCurrentStickyHeight())); + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.notebookOutline.entries, this.getCurrentStickyHeight())); } } private init() { this.notebookOutline.init(); - this.filteredOutlineEntries = this.notebookOutline.entries.filter(entry => entry.level !== 7); - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.filteredOutlineEntries, this.getCurrentStickyHeight())); + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.notebookOutline.entries, this.getCurrentStickyHeight())); this._disposables.add(this.notebookOutline.onDidChange(() => { - this.filteredOutlineEntries = this.notebookOutline.entries.filter(entry => entry.level !== 7); - const recompute = computeContent(this.notebookEditor, this.notebookCellList, this.filteredOutlineEntries, this.getCurrentStickyHeight()); + const recompute = computeContent(this.notebookEditor, this.notebookCellList, this.notebookOutline.entries, this.getCurrentStickyHeight()); if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { this.updateContent(recompute); } @@ -242,14 +210,14 @@ export class NotebookStickyScroll extends Disposable { this._disposables.add(this.notebookEditor.onDidAttachViewModel(() => { this.notebookOutline.init(); - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.filteredOutlineEntries, this.getCurrentStickyHeight())); + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.notebookOutline.entries, this.getCurrentStickyHeight())); })); this._disposables.add(this.notebookEditor.onDidScroll(() => { const d = new Delayer(100); d.trigger(() => { d.dispose(); - const recompute = computeContent(this.notebookEditor, this.notebookCellList, this.filteredOutlineEntries, this.getCurrentStickyHeight()); + const recompute = computeContent(this.notebookEditor, this.notebookCellList, this.notebookOutline.entries, this.getCurrentStickyHeight()); if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { this.updateContent(recompute); } @@ -384,6 +352,7 @@ export class NotebookStickyScroll extends Disposable { stickyHeader.innerText = entry.label; stickyElement.append(stickyFoldingIcon.domNode, stickyHeader); + return new NotebookStickyLine(stickyElement, stickyFoldingIcon, stickyHeader, entry, notebookEditor); } @@ -433,7 +402,7 @@ export function computeContent(notebookEditor: INotebookEditor, notebookCellList } cellEntry = NotebookStickyScroll.getVisibleOutlineEntry(currentIndex, notebookOutlineEntries); if (!cellEntry) { - return new Map(); + continue; } const nextCell = notebookEditor.cellAt(currentIndex + 1); @@ -445,7 +414,7 @@ export function computeContent(notebookEditor: INotebookEditor, notebookCellList } const nextCellEntry = NotebookStickyScroll.getVisibleOutlineEntry(currentIndex + 1, notebookOutlineEntries); if (!nextCellEntry) { - return new Map(); + continue; } // check next cell, if markdown with non level 7 entry, that means this is the end of the section (new header) --------------------- @@ -488,5 +457,3 @@ export function computeContent(notebookEditor: INotebookEditor, notebookCellList const newMap = NotebookStickyScroll.checkCollapsedStickyLines(cellEntry, linesToRender, notebookEditor); return newMap; } - -registerAction2(ToggleNotebookStickyScroll); diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 41136233486..25feb7f9123 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -402,7 +402,7 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { */ private getSuggestedLanguage(notebookTextModel: NotebookTextModel): string | undefined { const metaData = notebookTextModel.metadata; - let suggestedKernelLanguage: string | undefined = (metaData.custom as any)?.metadata?.language_info?.name; + let suggestedKernelLanguage: string | undefined = (metaData as any)?.metadata?.language_info?.name; // TODO how do we suggest multi language notebooks? if (!suggestedKernelLanguage) { const cellLanguages = notebookTextModel.cells.map(cell => cell.language).filter(language => language !== 'markdown'); diff --git a/code/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/code/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index 3291110ac93..45eccd656c3 100644 --- a/code/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/code/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -21,7 +21,9 @@ export interface ITextCellEditingDelegate { export class MoveCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Move Cell'; + get label() { + return this.length === 1 ? 'Move Cell' : 'Move Cells'; + } code: string = 'undoredo.notebooks.moveCell'; constructor( @@ -54,7 +56,17 @@ export class MoveCellEdit implements IResourceUndoRedoElement { export class SpliceCellsEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Insert Cell'; + get label() { + // Compute the most appropriate labels + if (this.diffs.length === 1 && this.diffs[0][1].length === 0) { + return this.diffs[0][2].length > 1 ? 'Insert Cells' : 'Insert Cell'; + } + if (this.diffs.length === 1 && this.diffs[0][2].length === 0) { + return this.diffs[0][1].length > 1 ? 'Delete Cells' : 'Delete Cell'; + } + // Default to Insert Cell + return 'Insert Cell'; + } code: string = 'undoredo.notebooks.insertCell'; constructor( public resource: URI, @@ -89,7 +101,7 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { export class CellMetadataEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = 'Update Cell Metadata'; - code: string = 'undoredo.notebooks.updateCellMetadata'; + code: string = 'undoredo.textBufferEdit'; constructor( public resource: URI, readonly index: number, diff --git a/code/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/code/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 2f72b627840..e78ccadce83 100644 --- a/code/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/code/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -25,17 +25,21 @@ import { isDefined } from 'vs/base/common/types'; class StackOperation implements IWorkspaceUndoRedoElement { type: UndoRedoElementType.Workspace; - readonly code = 'undoredo.notebooks.stackOperation'; + public get code() { + return this._operations.length === 1 ? this._operations[0].code : 'undoredo.notebooks.stackOperation'; + } private _operations: IUndoRedoElement[] = []; private _beginSelectionState: ISelectionState | undefined = undefined; private _resultSelectionState: ISelectionState | undefined = undefined; private _beginAlternativeVersionId: string; private _resultAlternativeVersionId: string; + public get label() { + return this._operations.length === 1 ? this._operations[0].label : 'edit'; + } constructor( readonly textModel: NotebookTextModel, - readonly label: string, readonly undoRedoGroup: UndoRedoGroup | undefined, private _pauseableEmitter: PauseableEmitter, private _postUndoRedo: (alternativeVersionId: string) => void, @@ -56,16 +60,18 @@ class StackOperation implements IWorkspaceUndoRedoElement { } pushEndState(alternativeVersionId: string, selectionState: ISelectionState | undefined) { + // https://github.com/microsoft/vscode/issues/207523 this._resultAlternativeVersionId = alternativeVersionId; - this._resultSelectionState = selectionState; + this._resultSelectionState = selectionState || this._resultSelectionState; } - pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined) { + pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined, alternativeVersionId: string) { if (this._operations.length === 0) { this._beginSelectionState = this._beginSelectionState ?? beginSelectionState; } this._operations.push(element); this._resultSelectionState = resultSelectionState; + this._resultAlternativeVersionId = alternativeVersionId; } async undo(): Promise { @@ -114,26 +120,20 @@ class NotebookOperationManager { return this._pendingStackOperation === null || this._pendingStackOperation.isEmpty; } - pushStackElement(label: string, selectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, alternativeVersionId: string) { - if (this._pendingStackOperation) { + pushStackElement(alternativeVersionId: string, selectionState: ISelectionState | undefined) { + if (this._pendingStackOperation && !this._pendingStackOperation.isEmpty) { this._pendingStackOperation.pushEndState(alternativeVersionId, selectionState); - if (!this._pendingStackOperation.isEmpty) { - this._undoService.pushElement(this._pendingStackOperation, this._pendingStackOperation.undoRedoGroup); - } - this._pendingStackOperation = null; - return; + this._undoService.pushElement(this._pendingStackOperation, this._pendingStackOperation.undoRedoGroup); } - - this._pendingStackOperation = new StackOperation(this._textModel, label, undoRedoGroup, this._pauseableEmitter, this._postUndoRedo, selectionState, alternativeVersionId); + this._pendingStackOperation = null; + } + private _getOrCreateEditStackElement(beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, alternativeVersionId: string) { + return this._pendingStackOperation ??= new StackOperation(this._textModel, undoRedoGroup, this._pauseableEmitter, this._postUndoRedo, beginSelectionState, alternativeVersionId || ''); } - pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined) { - if (this._pendingStackOperation) { - this._pendingStackOperation.pushEditOperation(element, beginSelectionState, resultSelectionState); - return; - } - - this._undoService.pushElement(element); + pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined, alternativeVersionId: string, undoRedoGroup: UndoRedoGroup | undefined) { + const pendingStackOperation = this._getOrCreateEditStackElement(beginSelectionState, undoRedoGroup, alternativeVersionId); + pendingStackOperation.pushEditOperation(element, beginSelectionState, resultSelectionState, alternativeVersionId); } } @@ -364,8 +364,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel super.dispose(); } - pushStackElement(label: string, selectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) { - this._operationManager.pushStackElement(label, selectionState, undoRedoGroup, this.alternativeVersionId); + pushStackElement() { + // https://github.com/microsoft/vscode/issues/207523 } private _getCellIndexByHandle(handle: number) { @@ -505,10 +505,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean): boolean { this._pauseableEmitter.pause(); - this.pushStackElement('edit', beginSelectionState, undoRedoGroup); + this._operationManager.pushStackElement(this._alternativeVersionId, undefined); try { - this._doApplyEdits(rawEdits, synchronous, computeUndoRedo); + this._doApplyEdits(rawEdits, synchronous, computeUndoRedo, beginSelectionState, undoRedoGroup); return true; } finally { // Update selection and versionId after applying edits. @@ -516,7 +516,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._increaseVersionId(this._operationManager.isUndoStackEmpty() && !this._pauseableEmitter.isDirtyEvent()); // Finalize undo element - this.pushStackElement('edit', endSelections, undefined); + this._operationManager.pushStackElement(this._alternativeVersionId, endSelections); // Broadcast changes this._pauseableEmitter.fire({ rawEvents: [], versionId: this.versionId, synchronous: synchronous, endSelectionState: endSelections }); @@ -524,7 +524,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - private _doApplyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, computeUndoRedo: boolean): void { + private _doApplyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined): void { const editsWithDetails = rawEdits.map((edit, index) => { let cellIndex: number = -1; if ('index' in edit) { @@ -606,7 +606,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel for (const { edit, cellIndex } of flattenEdits) { switch (edit.editType) { case CellEditType.Replace: - this._replaceCells(edit.index, edit.count, edit.cells, synchronous, computeUndoRedo); + this._replaceCells(edit.index, edit.count, edit.cells, synchronous, computeUndoRedo, beginSelectionState, undoRedoGroup); break; case CellEditType.Output: { this._assertIndex(cellIndex); @@ -632,11 +632,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel case CellEditType.Metadata: this._assertIndex(edit.index); - this._changeCellMetadata(this._cells[edit.index], edit.metadata, computeUndoRedo); + this._changeCellMetadata(this._cells[edit.index], edit.metadata, computeUndoRedo, beginSelectionState, undoRedoGroup); break; case CellEditType.PartialMetadata: this._assertIndex(cellIndex); - this._changeCellMetadataPartial(this._cells[cellIndex], edit.metadata, computeUndoRedo); + this._changeCellMetadataPartial(this._cells[cellIndex], edit.metadata, computeUndoRedo, beginSelectionState, undoRedoGroup); break; case CellEditType.PartialInternalMetadata: this._assertIndex(cellIndex); @@ -644,13 +644,13 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel break; case CellEditType.CellLanguage: this._assertIndex(edit.index); - this._changeCellLanguage(this._cells[edit.index], edit.language, computeUndoRedo); + this._changeCellLanguage(this._cells[edit.index], edit.language, computeUndoRedo, beginSelectionState, undoRedoGroup); break; case CellEditType.DocumentMetadata: - this._updateNotebookMetadata(edit.metadata, computeUndoRedo); + this._updateNotebookCellMetadata(edit.metadata, computeUndoRedo, beginSelectionState, undoRedoGroup); break; case CellEditType.Move: - this._moveCellToIdx(edit.index, edit.length, edit.newIdx, synchronous, computeUndoRedo, undefined, undefined); + this._moveCellToIdx(edit.index, edit.length, edit.newIdx, synchronous, computeUndoRedo, beginSelectionState, undefined, undoRedoGroup); break; } } @@ -695,7 +695,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return cellDto.collapseState ?? (defaultConfig ?? undefined); } - private _replaceCells(index: number, count: number, cellDtos: ICellDto2[], synchronous: boolean, computeUndoRedo: boolean): void { + private _replaceCells(index: number, count: number, cellDtos: ICellDto2[], synchronous: boolean, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined): void { if (count === 0 && cellDtos.length === 0) { return; @@ -763,7 +763,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel insertCell: (index, cell, endSelections) => { this._insertNewCell(index, [cell], true, endSelections); }, deleteCell: (index, endSelections) => { this._removeCell(index, 1, true, endSelections); }, replaceCell: (index, count, cells, endSelections) => { this._replaceNewCells(index, count, cells, true, endSelections); }, - }, undefined, undefined), undefined, undefined); + }, undefined, undefined), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup); } // should be deferred @@ -788,7 +788,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._notebookSpecificAlternativeId = Number(newAlternativeVersionId.substring(0, newAlternativeVersionId.indexOf('_'))); } - private _updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean) { + private _updateNotebookCellMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) { const oldMetadata = this.metadata; const triggerDirtyChange = this._isDocumentMetadataChanged(this.metadata, metadata); @@ -800,15 +800,15 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel get resource() { return that.uri; } - readonly label = 'Update Notebook Metadata'; - readonly code = 'undoredo.notebooks.updateCellMetadata'; + readonly label = 'Update Cell Metadata'; + readonly code = 'undoredo.textBufferEdit'; undo() { - that._updateNotebookMetadata(oldMetadata, false); + that._updateNotebookCellMetadata(oldMetadata, false, beginSelectionState, undoRedoGroup); } redo() { - that._updateNotebookMetadata(metadata, false); + that._updateNotebookCellMetadata(metadata, false, beginSelectionState, undoRedoGroup); } - }(), undefined, undefined); + }(), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup); } } @@ -950,7 +950,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } - private _changeCellMetadataPartial(cell: NotebookCellTextModel, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean) { + private _changeCellMetadataPartial(cell: NotebookCellTextModel, metadata: NullablePartialNotebookCellMetadata, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) { const newMetadata: NotebookCellMetadata = { ...cell.metadata }; @@ -960,10 +960,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel newMetadata[k] = value as any; } - return this._changeCellMetadata(cell, newMetadata, computeUndoRedo); + return this._changeCellMetadata(cell, newMetadata, computeUndoRedo, beginSelectionState, undoRedoGroup); } - private _changeCellMetadata(cell: NotebookCellTextModel, metadata: NotebookCellMetadata, computeUndoRedo: boolean) { + private _changeCellMetadata(cell: NotebookCellTextModel, metadata: NotebookCellMetadata, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) { const triggerDirtyChange = this._isCellMetadataChanged(cell.metadata, metadata); if (triggerDirtyChange) { @@ -975,9 +975,9 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel if (!cell) { return; } - this._changeCellMetadata(cell, newMetadata, false); + this._changeCellMetadata(cell, newMetadata, false, beginSelectionState, undoRedoGroup); } - }), undefined, undefined); + }), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup); } } @@ -1010,7 +1010,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel }); } - private _changeCellLanguage(cell: NotebookCellTextModel, languageId: string, computeUndoRedo: boolean) { + private _changeCellLanguage(cell: NotebookCellTextModel, languageId: string, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined) { if (cell.language === languageId) { return; } @@ -1028,12 +1028,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel readonly label = 'Update Cell Language'; readonly code = 'undoredo.notebooks.updateCellLanguage'; undo() { - that._changeCellLanguage(cell, oldLanguage, false); + that._changeCellLanguage(cell, oldLanguage, false, beginSelectionState, undoRedoGroup); } redo() { - that._changeCellLanguage(cell, languageId, false); + that._changeCellLanguage(cell, languageId, false, beginSelectionState, undoRedoGroup); } - }(), undefined, undefined); + }(), beginSelectionState, undefined, this._alternativeVersionId, undoRedoGroup); } this._pauseableEmitter.fire({ @@ -1121,13 +1121,13 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - private _moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: ISelectionState | undefined, endSelections: ISelectionState | undefined): boolean { + private _moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean, beforeSelections: ISelectionState | undefined, endSelections: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined): boolean { if (pushedToUndoStack) { this._operationManager.pushEditOperation(new MoveCellEdit(this.uri, index, length, newIdx, { moveCell: (fromIndex: number, length: number, toIndex: number, beforeSelections: ISelectionState | undefined, endSelections: ISelectionState | undefined) => { - this._moveCellToIdx(fromIndex, length, toIndex, true, false, beforeSelections, endSelections); + this._moveCellToIdx(fromIndex, length, toIndex, true, false, beforeSelections, endSelections, undoRedoGroup); }, - }, beforeSelections, endSelections), beforeSelections, endSelections); + }, beforeSelections, endSelections), beforeSelections, endSelections, this._alternativeVersionId, undoRedoGroup); } this._assertIndex(index); diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c673ae8ad79..37725f005dd 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -32,6 +32,7 @@ import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/serv import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IFileReadLimits } from 'vs/platform/files/common/files'; import { parse as parseUri, generate as generateUri } from 'vs/workbench/services/notebook/common/notebookDocumentService'; +import { ICellExecutionError } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; @@ -120,6 +121,7 @@ export interface NotebookCellInternalMetadata { runStartTimeAdjustment?: number; runEndTime?: number; renderDuration?: { [key: string]: number }; + error?: ICellExecutionError; } export interface NotebookCellCollapseState { @@ -948,7 +950,8 @@ export const NotebookSetting = { scrollToRevealCell: 'notebook.scrolling.revealNextCellOnExecute', anchorToFocusedCell: 'notebook.scrolling.experimental.anchorToFocusedCell', cellChat: 'notebook.experimental.cellChat', - notebookVariablesView: 'notebook.experimental.variablesView' + notebookVariablesView: 'notebook.experimental.variablesView', + InteractiveWindowPromptToSave: 'interactiveWindow.promptToSaveOnClose' } as const; export const enum CellStatusbarAlignment { diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/code/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 42b5d294c13..8345a520e5c 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -46,6 +46,7 @@ export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCel export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); export const NOTEBOOK_CELL_RESOURCE = new RawContextKey('notebookCellResource', ''); +export const NOTEBOOK_CELL_GENERATED_BY_CHAT = new RawContextKey('notebookCellGenerateByChat', false); // Kernels export const NOTEBOOK_KERNEL = new RawContextKey('notebookKernel', undefined); diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/code/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 7c1a89da760..293ca2e4795 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -42,23 +42,11 @@ export interface NotebookEditorInputOptions { export class NotebookEditorInput extends AbstractResourceEditorInput { - private static EditorCache: Record = {}; - static getOrCreate(instantiationService: IInstantiationService, resource: URI, preferredResource: URI | undefined, viewType: string, options: NotebookEditorInputOptions = {}) { - const cacheId = `${resource.toString()}|${viewType}|${options._workingCopy?.typeId}`; - let editor = NotebookEditorInput.EditorCache[cacheId]; - - if (!editor) { - editor = instantiationService.createInstance(NotebookEditorInput, resource, preferredResource, viewType, options); - NotebookEditorInput.EditorCache[cacheId] = editor; - - editor.onWillDispose(() => { - delete NotebookEditorInput.EditorCache[cacheId]; - }); - } else if (preferredResource) { + const editor = instantiationService.createInstance(NotebookEditorInput, resource, preferredResource, viewType, options); + if (preferredResource) { editor.setPreferredResource(preferredResource); } - return editor; } diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/code/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index f54d5f73092..e91e96ece99 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -52,11 +52,12 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE private readonly _hasAssociatedFilePath: boolean, readonly viewType: string, private readonly _workingCopyManager: IFileWorkingCopyManager, + scratchpad: boolean, @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService ) { super(); - this.scratchPad = viewType === 'interactive'; + this.scratchPad = scratchpad; } override dispose(): void { @@ -308,7 +309,7 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF } pushStackElement(): void { - this._notebookModel.pushStackElement('save', undefined, undefined); + this._notebookModel.pushStackElement(); } } diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/code/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index bbc69d31aa2..0a8a17a170d 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { CellUri, IResolvedNotebookEditorModel, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, IResolvedNotebookEditorModel, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -77,7 +77,8 @@ class NotebookModelReferenceCollection extends ReferenceCollection(NotebookSetting.InteractiveWindowPromptToSave) !== true; + const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, scratchPad); const result = await model.load({ limits }); diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts b/code/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts index 98a24d28adf..5b98e7ca262 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts @@ -5,7 +5,8 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IRange } from 'vs/editor/common/core/range'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { NotebookCellExecutionState, NotebookExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType, ICellExecuteOutputEdit, ICellExecuteOutputItemEdit } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -20,9 +21,16 @@ export interface ICellExecutionStateUpdate { isPaused?: boolean; } +export interface ICellExecutionError { + message: string; + stack: string | undefined; + uri: UriComponents; + location: IRange | undefined; +} export interface ICellExecutionComplete { runEndTime?: number; lastRunSuccess?: boolean; + error?: ICellExecutionError; } export enum NotebookExecutionType { cell, diff --git a/code/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/code/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index 6136451c28a..654fe7ee807 100644 --- a/code/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/code/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -35,9 +35,9 @@ suite('NotebookCommon', () => { test('diff different source', async () => { await withTestNotebookDiffModel([ - ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], ], [ - ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], ], (model, disposables, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); @@ -72,10 +72,10 @@ suite('NotebookCommon', () => { test('diff different output', async () => { await withTestNotebookDiffModel([ - ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 5 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], ['', 'javascript', CellKind.Code, [], {}] ], [ - ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], ['', 'javascript', CellKind.Code, [], {}] ], (model, disposables, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); @@ -197,12 +197,12 @@ suite('NotebookCommon', () => { test('diff foo/foe', async () => { await withTestNotebookDiffModel([ - [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 5 }], - [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 6 }], + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], ['', 'javascript', CellKind.Code, [], {}] ], [ - [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 5 }], - [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 6 }], + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'].join(''), 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], ['', 'javascript', CellKind.Code, [], {}] ], (model, disposables, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); @@ -407,15 +407,15 @@ suite('NotebookCommon', () => { test('LCS', async () => { await withTestNotebookDiffModel([ - ['# Description', 'markdown', CellKind.Markup, [], { custom: { metadata: {} } }], - ['x = 3', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: true } }, executionOrder: 1 }], - ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 1 }], - ['x', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: false } } }] + ['# Description', 'markdown', CellKind.Markup, [], { metadata: {} }], + ['x = 3', 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + ['x', 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }] ], [ - ['# Description', 'markdown', CellKind.Markup, [], { custom: { metadata: {} } }], - ['x = 3', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: true } }, executionOrder: 1 }], - ['x', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: false } } }], - ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 1 }] + ['# Description', 'markdown', CellKind.Markup, [], { metadata: {} }], + ['x = 3', 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + ['x', 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }] ], async (model) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); @@ -440,18 +440,18 @@ suite('NotebookCommon', () => { test('LCS 2', async () => { await withTestNotebookDiffModel([ - ['# Description', 'markdown', CellKind.Markup, [], { custom: { metadata: {} } }], - ['x = 3', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: true } }, executionOrder: 1 }], - ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 1 }], - ['x', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: false } } }], + ['# Description', 'markdown', CellKind.Markup, [], { metadata: {} }], + ['x = 3', 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + ['x', 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], ['x = 5', 'javascript', CellKind.Code, [], {}], ['x', 'javascript', CellKind.Code, [], {}], ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], ], [ - ['# Description', 'markdown', CellKind.Markup, [], { custom: { metadata: {} } }], - ['x = 3', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: true } }, executionOrder: 1 }], - ['x', 'javascript', CellKind.Code, [], { custom: { metadata: { collapsed: false } } }], - ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 1 }], + ['# Description', 'markdown', CellKind.Markup, [], { metadata: {} }], + ['x = 3', 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + ['x', 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], ['x = 5', 'javascript', CellKind.Code, [], {}], ['x', 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], ['x', 'javascript', CellKind.Code, [], {}], @@ -528,11 +528,11 @@ suite('NotebookCommon', () => { test('diff output', async () => { await withTestNotebookDiffModel([ - ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], - ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], ], [ - ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], - ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], ], (model, disposables, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); @@ -557,11 +557,11 @@ suite('NotebookCommon', () => { test('diff output fast check', async () => { await withTestNotebookDiffModel([ - ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], - ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], ], [ - ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], - ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], ], (model, disposables, accessor) => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); diff --git a/code/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts b/code/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts index 8de6a93e27e..fa082d78e23 100644 --- a/code/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts +++ b/code/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts @@ -94,6 +94,18 @@ suite('NotebookVariableDataSource', () => { assert.equal(variables[0].extHostId, parent.extHostId, 'ExtHostId should match the parent since we will use it to get the real children'); }); + test('Get children for very large list', async () => { + const parent = { kind: 'variable', notebook: notebookModel, id: '1', extHostId: 1, name: 'list', value: '[...]', hasNamedChildren: false, indexedChildrenCount: 1_000_000 } as INotebookVariableElement; + results = []; + + const groups = await dataSource.getChildren(parent); + const children = await dataSource.getChildren(groups[99]); + + assert(children.length === 100, 'We should have a full page of child groups'); + assert(!provideVariablesCalled, 'provideVariables should not be called'); + assert.equal(children[0].extHostId, parent.extHostId, 'ExtHostId should match the parent since we will use it to get the real children'); + }); + test('Cancel while enumerating through children', async () => { const parent = { kind: 'variable', notebook: notebookModel, id: '1', extHostId: 1, name: 'list', value: '[...]', hasNamedChildren: false, indexedChildrenCount: 10 } as INotebookVariableElement; results = [ diff --git a/code/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/code/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 6756a2dd0fe..d00db5854fb 100644 --- a/code/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/code/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -6,7 +6,7 @@ import 'vs/css!./outlinePane'; import * as dom from 'vs/base/browser/dom'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { TimeoutTimer } from 'vs/base/common/async'; +import { TimeoutTimer, timeout } from 'vs/base/common/async'; import { IDisposable, toDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { localize } from 'vs/nls'; @@ -304,7 +304,19 @@ export class OutlinePane extends ViewPane implements IOutlinePane { // feature: reveal outline selection in editor // on change -> reveal/select defining range - this._editorControlDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, e.editorOptions, e.sideBySide))); + let idPool = 0; + this._editorControlDisposables.add(tree.onDidOpen(async e => { + const myId = ++idPool; + const isDoubleClick = e.browserEvent?.type === 'dblclick'; + if (!isDoubleClick) { + // workaround for https://github.com/microsoft/vscode/issues/206424 + await timeout(150); + if (myId !== idPool) { + return; + } + } + await newOutline.reveal(e.element, e.editorOptions, e.sideBySide, isDoubleClick); + })); // feature: reveal editor selection in outline const revealActiveElement = () => { if (!this._outlineViewState.followCursor || !newOutline.activeElement) { diff --git a/code/src/vs/workbench/contrib/output/browser/output.contribution.ts b/code/src/vs/workbench/contrib/output/browser/output.contribution.ts index 28eba6fc109..6d3f4d9ad31 100644 --- a/code/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/code/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { OutputService } from 'vs/workbench/contrib/output/browser/outputServices'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from 'vs/workbench/services/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -30,6 +30,8 @@ import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { ILoggerService, LogLevel, LogLevelToLocalizedString, LogLevelToString } from 'vs/platform/log/common/log'; +import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -99,6 +101,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerOpenActiveOutputFileInAuxWindowAction(); this.registerShowLogsAction(); this.registerOpenLogFileAction(); + this.registerConfigureActiveOutputLogLevelAction(); } private registerSwitchOutputAction(): void { @@ -334,6 +337,78 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { return null; } + private registerConfigureActiveOutputLogLevelAction(): void { + const that = this; + const logLevelMenu = new MenuId('workbench.output.menu.logLevel'); + this._register(MenuRegistry.appendMenuItem(MenuId.ViewTitle, { + submenu: logLevelMenu, + title: nls.localize('logLevel.label', "Set Log Level..."), + group: 'navigation', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE), + icon: Codicon.gear, + order: 6 + })); + + let order = 0; + const registerLogLevel = (logLevel: LogLevel) => { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.output.activeOutputLogLevel.${logLevel}`, + title: LogLevelToLocalizedString(logLevel).value, + toggled: CONTEXT_ACTIVE_OUTPUT_LEVEL.isEqualTo(LogLevelToString(logLevel)), + menu: { + id: logLevelMenu, + order: order++, + group: '0_level' + } + }); + } + async run(accessor: ServicesAccessor): Promise { + const channel = that.outputService.getActiveChannel(); + if (channel) { + const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); + if (channelDescriptor?.log && channelDescriptor.file) { + return accessor.get(ILoggerService).setLogLevel(channelDescriptor.file, logLevel); + } + } + } + })); + }; + + registerLogLevel(LogLevel.Trace); + registerLogLevel(LogLevel.Debug); + registerLogLevel(LogLevel.Info); + registerLogLevel(LogLevel.Warning); + registerLogLevel(LogLevel.Error); + registerLogLevel(LogLevel.Off); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.output.activeOutputLogLevelDefault`, + title: nls.localize('logLevelDefault.label', "Set As Default"), + menu: { + id: logLevelMenu, + order, + group: '1_default' + }, + precondition: CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.negate() + }); + } + async run(accessor: ServicesAccessor): Promise { + const channel = that.outputService.getActiveChannel(); + if (channel) { + const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); + if (channelDescriptor?.log && channelDescriptor.file) { + const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.file); + return await accessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, channelDescriptor.extensionId); + } + } + } + })); + } + private registerShowLogsAction(): void { this._register(registerAction2(class extends Action2 { constructor() { diff --git a/code/src/vs/workbench/contrib/output/browser/outputServices.ts b/code/src/vs/workbench/contrib/output/browser/outputServices.ts index 26fed6ffc4e..eb83c902631 100644 --- a/code/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/code/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -5,15 +5,15 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT } from 'vs/workbench/services/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/browser/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; -import { ILogService } from 'vs/platform/log/common/log'; +import { ILogService, ILoggerService, LogLevelToString } from 'vs/platform/log/common/log'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IOutputChannelModel } from 'vs/workbench/contrib/output/common/outputChannelModel'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -21,6 +21,8 @@ import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { IOutputChannelModelService } from 'vs/workbench/contrib/output/common/outputChannelModelService'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -74,15 +76,20 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private readonly activeOutputChannelContext: IContextKey; private readonly activeFileOutputChannelContext: IContextKey; + private readonly activeOutputChannelLevelSettableContext: IContextKey; + private readonly activeOutputChannelLevelContext: IContextKey; + private readonly activeOutputChannelLevelIsDefaultContext: IContextKey; constructor( @IStorageService private readonly storageService: IStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextModelService textModelResolverService: ITextModelService, @ILogService private readonly logService: ILogService, + @ILoggerService private readonly loggerService: ILoggerService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IViewsService private readonly viewsService: IViewsService, @IContextKeyService contextKeyService: IContextKeyService, + @IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService ) { super(); this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, ''); @@ -91,6 +98,9 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this._register(this.onActiveOutputChannel(channel => this.activeOutputChannelContext.set(channel))); this.activeFileOutputChannelContext = CONTEXT_ACTIVE_FILE_OUTPUT.bindTo(contextKeyService); + this.activeOutputChannelLevelSettableContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE.bindTo(contextKeyService); + this.activeOutputChannelLevelContext = CONTEXT_ACTIVE_OUTPUT_LEVEL.bindTo(contextKeyService); + this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService); // Register as text model content provider for output textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this); @@ -115,6 +125,14 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } })); + this._register(this.loggerService.onDidChangeLogLevel(_level => { + this.setLevelContext(); + this.setLevelIsDefaultContext(); + })); + this._register(this.defaultLogLevelsService.onDidChangeDefaultLogLevels(() => { + this.setLevelIsDefaultContext(); + })); + this._register(this.lifecycleService.onDidShutdown(() => this.dispose())); } @@ -166,9 +184,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } private createChannel(id: string): OutputChannel { - const channelDisposables: IDisposable[] = []; const channel = this.instantiateChannel(id); - channel.model.onDispose(() => { + this._register(Event.once(channel.model.onDispose)(() => { if (this.activeChannel === channel) { const channels = this.getChannelDescriptors(); const channel = channels.length ? this.getChannel(channels[0].id) : undefined; @@ -179,8 +196,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } } Registry.as(Extensions.OutputChannels).removeChannel(id); - dispose(channelDisposables); - }, channelDisposables); + })); return channel; } @@ -194,9 +210,30 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.instantiationService.createInstance(OutputChannel, channelData); } + private setLevelContext(): void { + const descriptor = this.activeChannel?.outputChannelDescriptor; + const channelLogLevel = descriptor?.log ? this.loggerService.getLogLevel(descriptor.file) : undefined; + this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); + } + + private async setLevelIsDefaultContext(): Promise { + const descriptor = this.activeChannel?.outputChannelDescriptor; + if (descriptor?.log) { + const channelLogLevel = this.loggerService.getLogLevel(descriptor.file); + const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); + this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); + } else { + this.activeOutputChannelLevelIsDefaultContext.set(false); + } + } + private setActiveChannel(channel: OutputChannel | undefined): void { this.activeChannel = channel; - this.activeFileOutputChannelContext.set(!!channel?.outputChannelDescriptor?.file); + const descriptor = channel?.outputChannelDescriptor; + this.activeFileOutputChannelContext.set(!!descriptor?.file); + this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); + this.setLevelIsDefaultContext(); + this.setLevelContext(); if (this.activeChannel) { this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, this.activeChannel.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); diff --git a/code/src/vs/workbench/contrib/output/browser/outputView.ts b/code/src/vs/workbench/contrib/output/browser/outputView.ts index b7484919efa..53206dacd2e 100644 --- a/code/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/code/src/vs/workbench/contrib/output/browser/outputView.ts @@ -33,6 +33,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; export class OutputViewPane extends ViewPane { @@ -159,10 +160,9 @@ class OutputEditor extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, - @IFileService fileService: IFileService, - @IContextKeyService contextKeyService: IContextKeyService, + @IFileService fileService: IFileService ) { - super(OUTPUT_VIEW_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* TODO@bpasero this is wrong */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey)); } @@ -213,6 +213,10 @@ class OutputEditor extends AbstractTextResourceEditor { return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel"); } + protected override computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel(); + } + override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (this.input && input.matches(this.input)) { diff --git a/code/src/vs/workbench/contrib/output/common/outputLinkComputer.ts b/code/src/vs/workbench/contrib/output/common/outputLinkComputer.ts index efab6b06075..0de2f9f2e15 100644 --- a/code/src/vs/workbench/contrib/output/common/outputLinkComputer.ts +++ b/code/src/vs/workbench/contrib/output/common/outputLinkComputer.ts @@ -88,7 +88,7 @@ export class OutputLinkComputer { } for (const workspaceFolderVariant of workspaceFolderVariants) { - const validPathCharacterPattern = '[^\\s\\(\\):<>"]'; + const validPathCharacterPattern = '[^\\s\\(\\):<>\'"]'; const validPathCharacterOrSpacePattern = `(?:${validPathCharacterPattern}| ${validPathCharacterPattern})`; const pathPattern = `${validPathCharacterOrSpacePattern}+\\.${validPathCharacterPattern}+`; const strictPathPattern = `${validPathCharacterPattern}+`; diff --git a/code/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts b/code/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts index 1c92342f5bb..2f7988f5236 100644 --- a/code/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts +++ b/code/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts @@ -277,9 +277,9 @@ suite('OutputLinkProvider', () => { line = toOSPath(' at \'C:\\Users\\someone\\AppData\\Local\\Temp\\_monacodata_9888\\workspaces\\mankala\\Game.ts\' in'); result = OutputLinkComputer.detectLinks(line, 1, patterns, contextService); assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].url, contextService.toResource('/Game.ts\'').toString()); + assert.strictEqual(result[0].url, contextService.toResource('/Game.ts').toString()); assert.strictEqual(result[0].range.startColumn, 6); - assert.strictEqual(result[0].range.endColumn, 86); + assert.strictEqual(result[0].range.endColumn, 85); }); test('OutputLinkProvider - #106847', function () { diff --git a/code/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts b/code/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts index 2ea34f84b37..72451c4fdd7 100644 --- a/code/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts +++ b/code/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts @@ -30,7 +30,12 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib this._setupListener(); }, 60000)); - this._setupListener(); + + // Only log 1% of users selected randomly to reduce the volume of data + if (Math.random() <= 0.01) { + this._setupListener(); + } + } private _setupListener(): void { diff --git a/code/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/code/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 70920e27b23..2f92489d4b8 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -141,6 +141,7 @@ export class DefineKeybindingWidget extends Widget { private _keybindingInputWidget: KeybindingsSearchWidget; private _outputNode: HTMLElement; private _showExistingKeybindingsNode: HTMLElement; + private _keybindingDisposables = this._register(new DisposableStore()); private _chords: ResolvedKeybinding[] | null = null; private _isVisible: boolean = false; @@ -238,17 +239,18 @@ export class DefineKeybindingWidget extends Widget { } private onKeybinding(keybinding: ResolvedKeybinding[] | null): void { + this._keybindingDisposables.clear(); this._chords = keybinding; dom.clearNode(this._outputNode); dom.clearNode(this._showExistingKeybindingsNode); - const firstLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles); + const firstLabel = this._keybindingDisposables.add(new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles)); firstLabel.set(this._chords?.[0] ?? undefined); if (this._chords) { for (let i = 1; i < this._chords.length; i++) { this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to"))); - const chordLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles); + const chordLabel = this._keybindingDisposables.add(new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles)); chordLabel.set(this._chords[i]); } } diff --git a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index eec7a8ce974..480d9b5ab89 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -58,6 +58,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; const $ = DOM.$; @@ -106,6 +109,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP readonly overflowWidgetsDomNode: HTMLElement; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IKeybindingService private readonly keybindingsService: IKeybindingService, @@ -119,7 +123,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP @IStorageService storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(KeybindingsEditor.ID, telemetryService, themeService, storageService); + super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService); this.delayedFiltering = new Delayer(300); this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(!!this.keybindingFocusContextKey.get()))); @@ -897,6 +901,7 @@ class ActionsColumnRenderer implements ITableRenderer(extensionContainer, $('a.extension-label', { tabindex: 0 })); const extensionId = new HighlightedLabel(DOM.append(extensionContainer, $('.extension-id-container.code'))); - return { sourceColumn, sourceLabel, extensionLabel, extensionContainer, extensionId, disposables: new DisposableStore() }; + return { sourceColumn, sourceColumnHover, sourceLabel, extensionLabel, extensionContainer, extensionId, disposables: new DisposableStore() }; } renderElement(keybindingItemEntry: IKeybindingItemEntry, index: number, templateData: ISourceColumnTemplateData, height: number | undefined): void { @@ -1035,14 +1051,14 @@ class SourceColumnRenderer implements ITableRenderer { this.extensionsWorkbenchService.open(extension.identifier.value); @@ -1058,7 +1074,10 @@ class SourceColumnRenderer implements ITableRenderer .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-sibling, .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key, .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-value { - white-space: normal; - overflow-wrap: normal; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } .settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-value-checkbox { diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index f843df64aa3..adfccc8135c 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -219,7 +219,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon return accessor.get(IPreferencesService).openSettings(opts); } })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openSettings2', @@ -232,9 +232,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openSettings({ jsonEditor: false, ...args }); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openSettingsJson', @@ -247,10 +247,10 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openSettings({ jsonEditor: true, ...args }); } - }); + })); const that = this; - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openApplicationSettingsJson', @@ -266,10 +266,10 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openApplicationSettings({ jsonEditor: true, ...args }); } - }); + })); // Opens the User tab of the Settings editor - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openGlobalSettings', @@ -282,8 +282,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openUserSettings(args); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openRawDefaultSettings', @@ -295,9 +295,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor) { return accessor.get(IPreferencesService).openRawDefaultSettings(); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: ConfigureLanguageBasedSettingsAction.ID, @@ -309,8 +309,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor) { return accessor.get(IInstantiationService).createInstance(ConfigureLanguageBasedSettingsAction, ConfigureLanguageBasedSettingsAction.ID, ConfigureLanguageBasedSettingsAction.LABEL.value).run(); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openWorkspaceSettings', @@ -327,9 +327,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = typeof args === 'string' ? { query: args } : sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openWorkspaceSettings(args); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openAccessibilitySettings', @@ -344,8 +344,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon async run(accessor: ServicesAccessor) { await accessor.get(IPreferencesService).openSettings({ jsonEditor: false, query: '@tag:accessibility' }); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openWorkspaceSettingsFile', @@ -361,8 +361,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openWorkspaceSettings({ jsonEditor: true, ...args }); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openFolderSettings', @@ -383,8 +383,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon await preferencesService.openFolderSettings({ folderUri: workspaceFolder.uri, ...args }); } } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openFolderSettingsFile', @@ -405,8 +405,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon await preferencesService.openFolderSettings({ folderUri: workspaceFolder.uri, jsonEditor: true, ...args }); } } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: '_workbench.action.openFolderSettings', @@ -423,8 +423,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor, resource: URI) { return accessor.get(IPreferencesService).openFolderSettings({ folderUri: resource }); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, @@ -444,9 +444,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon accessor.get(IPreferencesService).openSettings({ jsonEditor: false, query: '@tag:usesOnlineServices' }); } } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FILTER_UNTRUSTED, @@ -456,9 +456,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor) { accessor.get(IPreferencesService).openWorkspaceSettings({ jsonEditor: false, query: `@tag:${REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG}` }); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_COMMAND_FILTER_TELEMETRY, @@ -473,7 +473,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon accessor.get(IPreferencesService).openSettings({ jsonEditor: false, query: '@tag:telemetry' }); } } - }); + })); this.registerSettingsEditorActions(); @@ -481,7 +481,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon .then(() => { const remoteAuthority = this.environmentService.remoteAuthority; const hostLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) || remoteAuthority; - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openRemoteSettings', @@ -497,8 +497,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openRemoteSettings(args); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openRemoteSettingsFile', @@ -514,7 +514,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon args = sanitizeOpenSettingsArgs(args); return accessor.get(IPreferencesService).openRemoteSettings({ jsonEditor: true, ...args }); } - }); + })); }); } @@ -532,7 +532,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon preferencesEditor?.focusSearch(); } - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_SEARCH, @@ -549,9 +549,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, @@ -571,9 +571,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon const preferencesEditor = getPreferencesEditor(accessor); preferencesEditor?.clearSearchResults(); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_FILE, @@ -591,9 +591,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon const preferencesEditor = getPreferencesEditor(accessor); preferencesEditor?.focusSettings(); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH, @@ -611,9 +611,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon const preferencesEditor = getPreferencesEditor(accessor); preferencesEditor?.focusSettings(); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST, @@ -633,9 +633,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon preferencesEditor.focusSettings(); } } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC, @@ -660,9 +660,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon preferencesEditor.focusTOC(); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL, @@ -686,9 +686,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon preferencesEditor.focusSettings(true); } } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, @@ -710,9 +710,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon preferencesEditor.showContextMenu(); } } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_UP, @@ -742,7 +742,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon preferencesEditor.focusSearch(); } } - }); + })); } private registerKeybindingsActions() { @@ -791,7 +791,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon group: '2_configuration', order: 4 })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openDefaultKeybindingsFile', @@ -803,8 +803,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor) { return accessor.get(IPreferencesService).openDefaultKeybindingsFile(); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openGlobalKeybindingsFile', @@ -824,8 +824,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon run(accessor: ServicesAccessor) { return accessor.get(IPreferencesService).openGlobalKeybindingSettings(true); } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, @@ -845,8 +845,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon editorPane.search('@source:system'); } } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, @@ -866,8 +866,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon editorPane.search('@source:extension'); } } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, @@ -887,8 +887,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon editorPane.search('@source:user'); } } - }); - registerAction2(class extends Action2 { + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ id: KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, @@ -906,9 +906,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon editorPane.clearSearchResults(); } } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY, @@ -928,7 +928,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon editorPane.clearKeyboardShortcutSearchHistory(); } } - }); + })); this.registerKeybindingEditorActions(); } @@ -1261,7 +1261,7 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo })); const openSettingsJsonWhen = ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON, @@ -1282,7 +1282,7 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo } return null; } - }); + })); } } diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 19a69d1f1a0..078adf7dfa9 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -7,44 +7,44 @@ import { EventHelper, getDomNodePagePosition } from 'vs/base/browser/dom'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStringDictionary } from 'vs/base/common/collections'; import { Emitter, Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; +import { isEqual } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import { 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 { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import * as editorCommon from 'vs/editor/common/editorCommon'; +import * as languages from 'vs/editor/common/languages'; import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import * as languages from 'vs/editor/common/languages'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationRegistry, IRegisteredConfigurationPropertySchema, overrideIdentifiersFromKey, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; +import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema, IConfigurationRegistry, IRegisteredConfigurationPropertySchema, OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMarkerData, IMarkerService, MarkerSeverity, MarkerTag } from 'vs/platform/markers/common/markers'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ThemeIcon } from 'vs/base/common/themables'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { RangeHighlightDecorations } from 'vs/workbench/browser/codeeditor'; import { settingsEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { EditPreferenceWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; +import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { isEqual } from 'vs/base/common/resources'; -import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; export interface IPreferencesRenderer extends IDisposable { render(): void; diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 30542844cc9..676d63374bb 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -34,6 +34,8 @@ import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, Workbenc import { settingsEditIcon, settingsScopeDropDownIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export class FolderSettingsActionViewItem extends BaseActionViewItem { private _folder: IWorkspaceFolder | null; @@ -41,6 +43,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { private container!: HTMLElement; private anchorElement!: HTMLElement; + private anchorElementHover!: ICustomHover; private labelElement!: HTMLElement; private detailsElement!: HTMLElement; private dropDownElement!: HTMLElement; @@ -87,6 +90,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { 'aria-haspopup': 'true', 'tabindex': '0' }, this.labelElement, this.detailsElement, this.dropDownElement); + this.anchorElementHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.anchorElement, '')); this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.MOUSE_DOWN, e => DOM.EventHelper.stop(e))); this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.CLICK, e => this.onClick(e))); this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => this.onKeyUp(e))); @@ -145,7 +149,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { const workspace = this.contextService.getWorkspace(); if (this._folder) { this.labelElement.textContent = this._folder.name; - this.anchorElement.title = this._folder.name; + this.anchorElementHover.update(this._folder.name); const detailsText = this.labelWithCount(this._action.label, total); this.detailsElement.textContent = detailsText; this.dropDownElement.classList.toggle('hide', workspace.folders.length === 1 || !this._action.checked); @@ -153,7 +157,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { const labelText = this.labelWithCount(this._action.label, total); this.labelElement.textContent = labelText; this.detailsElement.textContent = ''; - this.anchorElement.title = this._action.label; + this.anchorElementHover.update(this._action.label); this.dropDownElement.classList.remove('hide'); } diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 33c9e99303b..3a5a03a9718 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -66,6 +66,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { CodeWindow } from 'vs/base/browser/window'; export const enum SettingsFocusContext { @@ -219,6 +220,7 @@ export class SettingsEditor2 extends EditorPane { private installedExtensionIds: string[] = []; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -240,7 +242,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, ) { - super(SettingsEditor2.ID, telemetryService, themeService, storageService); + super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); this.localSearchDelayer = new Delayer(300); this.remoteSearchThrottle = new ThrottledDelayer(200); @@ -398,7 +400,7 @@ export class SettingsEditor2 extends EditorPane { } private restoreCachedState(): ISettingsEditor2State | null { - const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input); + const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input); if (cachedState && typeof cachedState.target === 'object') { cachedState.target = URI.revive(cachedState.target); } @@ -499,8 +501,8 @@ export class SettingsEditor2 extends EditorPane { } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (!visible) { // Wait for editor to be removed from DOM #106303 @@ -645,7 +647,7 @@ export class SettingsEditor2 extends EditorPane { })); if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) { - const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer)); + const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; this.updateInputAriaLabel(); @@ -1426,7 +1428,7 @@ export class SettingsEditor2 extends EditorPane { // If the context view is focused, delay rendering settings if (this.contextViewFocused()) { - const element = DOM.getWindow(this.settingsTree.getHTMLElement()).document.querySelector('.context-view'); + const element = this.window.document.querySelector('.context-view'); if (element) { this.scheduleRefresh(element as HTMLElement, key); } @@ -1830,10 +1832,10 @@ export class SettingsEditor2 extends EditorPane { if (this.isVisible()) { const searchQuery = this.searchWidget.getValue().trim(); const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget; - if (this.group && this.input) { + if (this.input) { this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target }); } - } else if (this.group && this.input) { + } else if (this.input) { this.editorMemento.clearEditorState(this.input, this.group); } @@ -1849,6 +1851,7 @@ class SyncControls extends Disposable { public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event; constructor( + window: CodeWindow, container: HTMLElement, @ICommandService private readonly commandService: ICommandService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -1881,7 +1884,7 @@ class SyncControls extends Disposable { })); const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer()); - updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, DOM.getWindow(container)); + updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window); this.update(); this._register(this.userDataSyncService.onDidChangeStatus(() => { diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 2f32b9c5932..fd8f621714f 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -22,7 +22,7 @@ import { SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/bro import { POLICY_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IHoverOptions, IHoverService } from 'vs/platform/hover/browser/hover'; -import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; const $ = DOM.$; @@ -135,12 +135,12 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { } private createWorkspaceTrustIndicator(): SettingIndicator { + const disposables = new DisposableStore(); const workspaceTrustElement = $('span.setting-indicator.setting-item-workspace-trust'); - const workspaceTrustLabel = new SimpleIconLabel(workspaceTrustElement); + const workspaceTrustLabel = disposables.add(new SimpleIconLabel(workspaceTrustElement)); workspaceTrustLabel.text = '$(warning) ' + localize('workspaceUntrustedLabel', "Setting value not applied"); const content = localize('trustLabel', "The setting value can only be applied in a trusted workspace."); - const disposables = new DisposableStore(); const showHover = (focus: boolean) => { return this.hoverService.showHover({ ...this.defaultHoverOptions, @@ -164,23 +164,24 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { } private createScopeOverridesIndicator(): SettingIndicator { + const disposables = new DisposableStore(); // Don't add .setting-indicator class here, because it gets conditionally added later. const otherOverridesElement = $('span.setting-item-overrides'); - const otherOverridesLabel = new SimpleIconLabel(otherOverridesElement); + const otherOverridesLabel = disposables.add(new SimpleIconLabel(otherOverridesElement)); return { element: otherOverridesElement, label: otherOverridesLabel, - disposables: new DisposableStore() + disposables }; } private createSyncIgnoredIndicator(): SettingIndicator { + const disposables = new DisposableStore(); const syncIgnoredElement = $('span.setting-indicator.setting-item-ignored'); - const syncIgnoredLabel = new SimpleIconLabel(syncIgnoredElement); + const syncIgnoredLabel = disposables.add(new SimpleIconLabel(syncIgnoredElement)); syncIgnoredLabel.text = localize('extensionSyncIgnoredLabel', 'Not synced'); const syncIgnoredHoverContent = localize('syncIgnoredTitle', "This setting is ignored during sync"); - const disposables = new DisposableStore(); const showHover = (focus: boolean) => { return this.hoverService.showHover({ ...this.defaultHoverOptions, @@ -193,19 +194,20 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { return { element: syncIgnoredElement, label: syncIgnoredLabel, - disposables: new DisposableStore() + disposables }; } private createDefaultOverrideIndicator(): SettingIndicator { + const disposables = new DisposableStore(); const defaultOverrideIndicator = $('span.setting-indicator.setting-item-default-overridden'); - const defaultOverrideLabel = new SimpleIconLabel(defaultOverrideIndicator); + const defaultOverrideLabel = disposables.add(new SimpleIconLabel(defaultOverrideIndicator)); defaultOverrideLabel.text = localize('defaultOverriddenLabel', "Default value changed"); return { element: defaultOverrideIndicator, label: defaultOverrideLabel, - disposables: new DisposableStore() + disposables }; } diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 8a24222eac3..9b7e714c4ce 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -69,6 +69,8 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { getInvalidTypeError } from 'vs/workbench/services/preferences/common/preferencesValidation'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = DOM.$; @@ -796,13 +798,13 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const labelCategoryContainer = DOM.append(titleElement, $('.setting-item-cat-label-container')); const categoryElement = DOM.append(labelCategoryContainer, $('span.setting-item-category')); const labelElementContainer = DOM.append(labelCategoryContainer, $('span.setting-item-label')); - const labelElement = new SimpleIconLabel(labelElementContainer); + const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer)); const indicatorsLabel = this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement); toDispose.add(indicatorsLabel); const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "The setting has been configured in the current scope."); + toDispose.add(setupCustomHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, () => localize('modified', "The setting has been configured in the current scope."))); const valueElement = DOM.append(container, $('.setting-item-value')); const controlElement = DOM.append(valueElement, $('div.setting-item-control')); @@ -889,7 +891,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); template.categoryElement.textContent = element.displayCategory ? (element.displayCategory + ': ') : ''; - template.categoryElement.title = titleTooltip; + template.elementDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), template.categoryElement, titleTooltip)); template.labelElement.text = element.displayLabel; template.labelElement.title = titleTooltip; @@ -1817,24 +1819,25 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre _container.classList.add('setting-item'); _container.classList.add('setting-item-bool'); + const toDispose = new DisposableStore(); + const container = DOM.append(_container, $(AbstractSettingRenderer.CONTENTS_SELECTOR)); container.classList.add('settings-row-inner-container'); const titleElement = DOM.append(container, $('.setting-item-title')); const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); const labelElementContainer = DOM.append(titleElement, $('span.setting-item-label')); - const labelElement = new SimpleIconLabel(labelElementContainer); + const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer)); const indicatorsLabel = this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement); const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "The setting has been configured in the current scope."); + toDispose.add(setupCustomHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, localize('modified', "The setting has been configured in the current scope."))); const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); - const toDispose = new DisposableStore(); const checkbox = new Toggle({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: true, title: '', ...unthemedToggleStyles }); controlElement.appendChild(checkbox.domNode); toDispose.add(checkbox); diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 6675881177f..3cf230fa6ee 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -27,6 +27,8 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = DOM.$; @@ -673,8 +675,8 @@ export class ListSettingWidget extends AbstractListSettingWidget : localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling); const { rowElement } = rowElementGroup; - rowElement.title = title; - rowElement.setAttribute('aria-label', rowElement.title); + this.listDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + rowElement.setAttribute('aria-label', title); } protected getLocalizedStrings() { @@ -733,8 +735,8 @@ export class ExcludeSettingWidget extends ListSettingWidget { : localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", value.data, sibling); const { rowElement } = rowElementGroup; - rowElement.title = title; - rowElement.setAttribute('aria-label', rowElement.title); + this.listDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + rowElement.setAttribute('aria-label', title); } protected override getLocalizedStrings() { @@ -763,8 +765,8 @@ export class IncludeSettingWidget extends ListSettingWidget { : localize('includeSiblingHintLabel', "Include files matching `{0}`, only when a file matching `{1}` is present", value.data, sibling); const { rowElement } = rowElementGroup; - rowElement.title = title; - rowElement.setAttribute('aria-label', rowElement.title); + this.listDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + rowElement.setAttribute('aria-label', title); } protected override getLocalizedStrings() { @@ -1161,10 +1163,10 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget { @@ -111,17 +115,20 @@ export class TOCRenderer implements ITreeRenderer, index: number, template: ITOCEntryTemplate): void { + template.elementDisposables.clear(); + const element = node.element; const count = element.count; const label = element.label; template.labelElement.textContent = label; - template.labelElement.title = label; + template.elementDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), template.labelElement, label)); if (count) { template.countElement.textContent = ` (${count})`; @@ -131,6 +138,7 @@ export class TOCRenderer implements ITreeRenderer(PORT_AUTO_FALLBACK_SETTING); + if ((fallbackAt.value !== undefined) && (fallbackAt.value === 0 || (fallbackAt.value !== fallbackAt.defaultValue))) { + return fallbackAt.value; + } + const inspectSource = this.configurationService.inspect(PORT_AUTO_SOURCE_SETTING); + if (inspectSource.applicationValue === PORT_AUTO_SOURCE_SETTING_PROCESS || + inspectSource.userValue === PORT_AUTO_SOURCE_SETTING_PROCESS || + inspectSource.userLocalValue === PORT_AUTO_SOURCE_SETTING_PROCESS || + inspectSource.userRemoteValue === PORT_AUTO_SOURCE_SETTING_PROCESS || + inspectSource.workspaceFolderValue === PORT_AUTO_SOURCE_SETTING_PROCESS || + inspectSource.workspaceValue === PORT_AUTO_SOURCE_SETTING_PROCESS) { + return 0; + } + return fallbackAt.value ?? 20; + } + private listenForPorts() { - let fallbackAt = this.configurationService.getValue(PORT_AUTO_FALLBACK_SETTING); + let fallbackAt = this.getPortAutoFallbackNumber(); if (fallbackAt === 0) { this.portListener?.dispose(); return; @@ -226,7 +243,7 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon if (this.procForwarder && !this.portListener && (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS)) { this.portListener = this._register(this.remoteExplorerService.tunnelModel.onForwardPort(async () => { - fallbackAt = this.configurationService.getValue(PORT_AUTO_FALLBACK_SETTING); + fallbackAt = this.getPortAutoFallbackNumber(); if (fallbackAt === 0) { this.portListener?.dispose(); return; @@ -269,8 +286,10 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon this.outputForwarder?.dispose(); this.outputForwarder = undefined; if (environment?.os !== OperatingSystem.Linux) { - Registry.as(ConfigurationExtensions.Configuration) - .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + if (this.configurationService.inspect(PORT_AUTO_SOURCE_SETTING).default?.value !== PORT_AUTO_SOURCE_SETTING_OUTPUT) { + Registry.as(ConfigurationExtensions.Configuration) + .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + } this.outputForwarder = this._register(new OutputAutomaticPortForwarding(this.terminalService, this.notificationService, this.openerService, this.externalOpenerService, this.remoteExplorerService, this.configurationService, this.debugService, this.tunnelService, this.hostService, this.logService, this.contextKeyService, () => false)); } else { diff --git a/code/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/code/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index f362b61aa1b..ebb65e74664 100644 --- a/code/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/code/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -97,7 +97,32 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr private measureNetworkConnectionLatencyScheduler: RunOnceScheduler | undefined = undefined; private loggedInvalidGroupNames: { [group: string]: boolean } = Object.create(null); - private readonly remoteExtensionMetadata: RemoteExtensionMetadata[]; + + private _remoteExtensionMetadata: RemoteExtensionMetadata[] | undefined = undefined; + private get remoteExtensionMetadata(): RemoteExtensionMetadata[] { + if (!this._remoteExtensionMetadata) { + const remoteExtensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips }; + this._remoteExtensionMetadata = Object.values(remoteExtensionTips).filter(value => value.startEntry !== undefined).map(value => { + return { + id: value.extensionId, + installed: false, + friendlyName: value.friendlyName, + isPlatformCompatible: false, + dependencies: [], + helpLink: value.startEntry?.helpLink ?? '', + startConnectLabel: value.startEntry?.startConnectLabel ?? '', + startCommand: value.startEntry?.startCommand ?? '', + priority: value.startEntry?.priority ?? 10, + supportedPlatforms: value.supportedPlatforms + }; + }); + + this.remoteExtensionMetadata.sort((ext1, ext2) => ext1.priority - ext2.priority); + } + + return this._remoteExtensionMetadata; + } + private remoteMetadataInitialized: boolean = false; private readonly _onDidChangeEntries = this._register(new Emitter()); private readonly onDidChangeEntries: Event = this._onDidChangeEntries.event; @@ -124,24 +149,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr ) { super(); - const remoteExtensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips }; - this.remoteExtensionMetadata = Object.values(remoteExtensionTips).filter(value => value.startEntry !== undefined).map(value => { - return { - id: value.extensionId, - installed: false, - friendlyName: value.friendlyName, - isPlatformCompatible: false, - dependencies: [], - helpLink: value.startEntry?.helpLink ?? '', - startConnectLabel: value.startEntry?.startConnectLabel ?? '', - startCommand: value.startEntry?.startCommand ?? '', - priority: value.startEntry?.priority ?? 10, - supportedPlatforms: value.supportedPlatforms - }; - }); - - this.remoteExtensionMetadata.sort((ext1, ext2) => ext1.priority - ext2.priority); - // Set initial connection state if (this.remoteAuthority) { this.connectionState = 'initializing'; @@ -162,7 +169,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr // Show Remote Menu const that = this; - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID, @@ -176,11 +183,11 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr }); } run = () => that.showRemoteMenu(); - }); + })); // Close Remote Connection if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID) { - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: RemoteStatusIndicator.CLOSE_REMOTE_COMMAND_ID, @@ -191,7 +198,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr }); } run = () => that.hostService.openWindow({ forceReuseWindow: true, remoteAuthority: null }); - }); + })); if (this.remoteAuthority) { MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '6_close', @@ -205,7 +212,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } if (this.extensionGalleryService.isEnabled()) { - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: RemoteStatusIndicator.INSTALL_REMOTE_EXTENSIONS_ID, @@ -223,7 +230,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } }); }; - }); + })); } } diff --git a/code/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts b/code/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts index e4e2f80af4b..551a983f173 100644 --- a/code/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts +++ b/code/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts @@ -48,7 +48,7 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi // Show Remote Start Action const startEntry = this; - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: RemoteStartEntry.REMOTE_WEB_START_ENTRY_ACTIONS_COMMAND_ID, @@ -61,7 +61,7 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi async run(): Promise { await startEntry.showWebRemoteStartActions(); } - }); + })); } private registerListeners(): void { diff --git a/code/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/code/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 8b0318487b6..49a702fbbfe 100644 --- a/code/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/code/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -44,19 +44,19 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { copyAddressIcon, forwardedPortWithoutProcessIcon, forwardedPortWithProcessIcon, forwardPortIcon, labelPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, privatePortIcon, stopForwardIcon } from 'vs/workbench/contrib/remote/browser/remoteIcons'; import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { isMacintosh } from 'vs/base/common/platform'; import { ITableColumn, ITableContextMenuEvent, ITableEvent, ITableMouseEvent, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; import { Button } from 'vs/base/browser/ui/button/button'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; 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'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export const openPreviewEnabledContext = new RawContextKey('openPreviewEnabled', false); @@ -1372,9 +1372,9 @@ export namespace OpenPortInPreviewAction { if (tunnel) { const remoteHost = tunnel.remoteHost.includes(':') ? `[${tunnel.remoteHost}]` : tunnel.remoteHost; const sourceUri = URI.parse(`http://${remoteHost}:${tunnel.remotePort}`); - const opener = await externalOpenerService.getOpener(tunnel.localUri, { sourceUri }, new CancellationTokenSource().token); + const opener = await externalOpenerService.getOpener(tunnel.localUri, { sourceUri }, CancellationToken.None); if (opener) { - return opener.openExternalUri(tunnel.localUri, { sourceUri }, new CancellationTokenSource().token); + return opener.openExternalUri(tunnel.localUri, { sourceUri }, CancellationToken.None); } return openerService.open(tunnel.localUri); } diff --git a/code/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/code/src/vs/workbench/contrib/remote/common/remote.contribution.ts index c66080cacec..446aef1fc36 100644 --- a/code/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/code/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -239,7 +239,7 @@ Registry.as(ConfigurationExtensions.Configuration) 'remote.autoForwardPortsFallback': { type: 'number', default: 20, - markdownDescription: localize('remote.autoForwardPortFallback', "The number of auto forwarded ports that will trigger the switch from `process` to `hybrid` when automatically forwarding ports and `remote.autoForwardPortsSource` is set to `process`. Set to `0` to disable the fallback.") + markdownDescription: localize('remote.autoForwardPortFallback', "The number of auto forwarded ports that will trigger the switch from `process` to `hybrid` when automatically forwarding ports and `remote.autoForwardPortsSource` is set to `process` by default. Set to `0` to disable the fallback. When `remote.autoForwardPortsFallback` hasn't been configured, but `remote.autoForwardPortsSource` has, `remote.autoForwardPortsFallback` will be treated as though it's set to `0`.") }, 'remote.forwardOnOpen': { type: 'boolean', diff --git a/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 305f9bae1c5..0f750170457 100644 --- a/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -395,7 +395,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private createExistingSessionItem(session: AuthenticationSession, providerId: string): ExistingSessionItem { return { label: session.account.label, - description: this.authenticationService.getLabel(providerId), + description: this.authenticationService.getProvider(providerId).label, session, providerId }; @@ -412,9 +412,9 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); - options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", providerName), provider: authenticationProvider }); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!signedInForProvider || provider.supportsMultipleAccounts) { + options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", provider.label), provider: authenticationProvider }); } } @@ -797,6 +797,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis description: localize('remoteTunnelAccess.machineName', "The name under which the remote tunnel access is registered. If not set, the host name is used."), type: 'string', scope: ConfigurationScope.APPLICATION, + ignoreSync: true, pattern: '^(\\w[\\w-]*)?$', patternErrorMessage: localize('remoteTunnelAccess.machineNameRegex', "The name must only consist of letters, numbers, underscore and dash. It must not start with a dash."), maxLength: 20, diff --git a/code/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/code/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index adcf99c35cd..7537f80f534 100644 --- a/code/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/code/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1568,12 +1568,12 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor this.onDidChangeConfiguration(); const onDidChangeDiffWidthConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterWidth')); - onDidChangeDiffWidthConfiguration(this.onDidChangeDiffWidthConfiguration, this); + this._register(onDidChangeDiffWidthConfiguration(this.onDidChangeDiffWidthConfiguration, this)); this.onDidChangeDiffWidthConfiguration(); const onDidChangeDiffVisibilityConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterVisibility')); - onDidChangeDiffVisibilityConfiguration(this.onDidChangeDiffVisibiltiyConfiguration, this); - this.onDidChangeDiffVisibiltiyConfiguration(); + this._register(onDidChangeDiffVisibilityConfiguration(this.onDidChangeDiffVisibilityConfiguration, this)); + this.onDidChangeDiffVisibilityConfiguration(); } private onDidChangeConfiguration(): void { @@ -1596,7 +1596,7 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor this.setViewState({ ...this.viewState, width }); } - private onDidChangeDiffVisibiltiyConfiguration(): void { + private onDidChangeDiffVisibilityConfiguration(): void { const visibility = this.configurationService.getValue<'always' | 'hover'>('scm.diffDecorationsGutterVisibility'); this.setViewState({ ...this.viewState, visibility }); } diff --git a/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index f35fe3e0eda..544b8d274a7 100644 --- a/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/code/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -67,7 +67,7 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILabelService } from 'vs/platform/label/common/label'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; @@ -372,6 +372,9 @@ class InputRenderer implements ICompressibleTreeRenderer { const contentHeight = templateData.inputWidget.getContentHeight(); diff --git a/code/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.contribution.ts b/code/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.contribution.ts new file mode 100644 index 00000000000..ad8bc56733b --- /dev/null +++ b/code/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.contribution.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { SyncScroll as ScrollLocking } from 'vs/workbench/contrib/scrollLocking/browser/scrollLocking'; + +registerWorkbenchContribution2( + ScrollLocking.ID, + ScrollLocking, + WorkbenchPhase.Eventually // registration only +); diff --git a/code/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.ts b/code/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.ts new file mode 100644 index 00000000000..98dc65d9c36 --- /dev/null +++ b/code/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * 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, toDisposable } from 'vs/base/common/lifecycle'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize, localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IEditorPane, IEditorPaneScrollPosition, isEditorPaneWithScrolling } from 'vs/workbench/common/editor'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; + +class SyncScrollStatusEntry extends Disposable { + + private readonly syncScrollEntry = this._register(new MutableDisposable()); + + constructor(@IStatusbarService private readonly statusbarService: IStatusbarService) { + super(); + } + + updateSyncScroll(visible: boolean): void { + if (visible) { + if (!this.syncScrollEntry.value) { + this.syncScrollEntry.value = this.statusbarService.addEntry({ + name: 'Scrolling Locked', + text: 'Scrolling Locked', + tooltip: 'Lock Scrolling enabled', + ariaLabel: 'Scrolling Locked', + command: { + id: 'workbench.action.toggleLockedScrolling', + title: '' + }, + kind: 'prominent' + }, 'status.scrollLockingEnabled', StatusbarAlignment.RIGHT, 102); + } + } else { + this.syncScrollEntry.clear(); + } + } +} + +export class SyncScroll extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.syncScrolling'; + + private readonly paneInitialScrollTop = new Map(); + + private readonly syncScrollDispoasbles = this._register(new DisposableStore()); + private readonly paneDisposables = new DisposableStore(); + + private statusBarEntries = new Set(); + + private isActive: boolean = false; + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + + this.registerActions(); + } + + private registerActiveListeners(): void { + this.syncScrollDispoasbles.add(this.editorService.onDidVisibleEditorsChange(() => this.trackVisiblePanes())); + } + + private activate(): void { + this.registerActiveListeners(); + + this.trackVisiblePanes(); + } + + toggle(): void { + if (this.isActive) { + this.deactivate(); + } else { + this.activate(); + } + + this.isActive = !this.isActive; + + this.toggleStatusbarItem(this.isActive); + } + + private trackVisiblePanes(): void { + this.paneDisposables.clear(); + this.paneInitialScrollTop.clear(); + + for (const pane of this.getAllVisiblePanes()) { + + if (!isEditorPaneWithScrolling(pane)) { + continue; + } + + this.paneInitialScrollTop.set(pane, pane.getScrollPosition()); + this.paneDisposables.add(pane.onDidChangeScroll(() => this.onDidEditorPaneScroll(pane))); + } + } + + private onDidEditorPaneScroll(scrolledPane: IEditorPane) { + + const scrolledPaneInitialOffset = this.paneInitialScrollTop.get(scrolledPane); + if (scrolledPaneInitialOffset === undefined) { + throw new Error('Scrolled pane not tracked'); + } + + if (!isEditorPaneWithScrolling(scrolledPane)) { + throw new Error('Scrolled pane does not support scrolling'); + } + + const scrolledPaneCurrentPosition = scrolledPane.getScrollPosition(); + const scrolledFromInitial = { + scrollTop: scrolledPaneCurrentPosition.scrollTop - scrolledPaneInitialOffset.scrollTop, + scrollLeft: scrolledPaneCurrentPosition.scrollLeft !== undefined && scrolledPaneInitialOffset.scrollLeft !== undefined ? scrolledPaneCurrentPosition.scrollLeft - scrolledPaneInitialOffset.scrollLeft : undefined, + }; + + for (const pane of this.getAllVisiblePanes()) { + if (pane === scrolledPane) { + continue; + } + + if (!isEditorPaneWithScrolling(pane)) { + return; + } + + const initialOffset = this.paneInitialScrollTop.get(pane); + if (initialOffset === undefined) { + throw new Error('Could not find initial offset for pane'); + } + + pane.setScrollPosition({ + scrollTop: initialOffset.scrollTop + scrolledFromInitial.scrollTop, + scrollLeft: initialOffset.scrollLeft !== undefined && scrolledFromInitial.scrollLeft !== undefined ? initialOffset.scrollLeft + scrolledFromInitial.scrollLeft : undefined, + }); + } + } + + private getAllVisiblePanes(): IEditorPane[] { + const panes: IEditorPane[] = []; + + for (const pane of this.editorService.visibleEditorPanes) { + + if (pane instanceof SideBySideEditor) { + const primaryPane = pane.getPrimaryEditorPane(); + const secondaryPane = pane.getSecondaryEditorPane(); + if (primaryPane) { + panes.push(primaryPane); + } + if (secondaryPane) { + panes.push(secondaryPane); + } + continue; + } + + panes.push(pane); + } + + return panes; + } + + private deactivate(): void { + this.paneDisposables.clear(); + this.syncScrollDispoasbles.clear(); + this.paneInitialScrollTop.clear(); + } + + // Actions & Commands + + private createStatusBarItem(instantiationService: IInstantiationService, disposables: DisposableStore): SyncScrollStatusEntry { + const entry = disposables.add(instantiationService.createInstance(SyncScrollStatusEntry)); + + this.statusBarEntries.add(entry); + disposables.add(toDisposable(() => this.statusBarEntries.delete(entry))); + + return entry; + } + + private registerStatusBarItems() { + const entry = this.createStatusBarItem(this.instantiationService, this._store); + entry.updateSyncScroll(this.isActive); + + this._register(this.editorGroupsService.onDidCreateAuxiliaryEditorPart(({ instantiationService, disposables }) => { + const entry = this.createStatusBarItem(instantiationService, disposables); + entry.updateSyncScroll(this.isActive); + })); + } + + private toggleStatusbarItem(active: boolean): void { + for (const item of this.statusBarEntries) { + item.updateSyncScroll(active); + } + } + + private registerActions() { + const $this = this; + + this.registerStatusBarItems(); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.toggleLockedScrolling', + title: { + ...localize2('toggleLockedScrolling', "Toggle Locked Scrolling Across Editors"), + mnemonicTitle: localize({ key: 'miToggleLockedScrolling', comment: ['&& denotes a mnemonic'] }, "Locked Scrolling"), + }, + category: Categories.View, + f1: true + }); + } + + run(): void { + $this.toggle(); + } + })); + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.holdLockedScrolling', + title: { + ...localize2('holdLockedScrolling', "Hold Locked Scrolling Across Editors"), + mnemonicTitle: localize({ key: 'miHoldLockedScrolling', comment: ['&& denotes a mnemonic'] }, "Locked Scrolling"), + }, + category: Categories.View, + }); + } + + run(accessor: ServicesAccessor): void { + const keybindingService = accessor.get(IKeybindingService); + + // Enable Sync Scrolling while pressed + $this.toggle(); + + const holdMode = keybindingService.enableKeybindingHoldMode('workbench.action.holdLockedScrolling'); + if (!holdMode) { + return; + } + + holdMode.finally(() => { + $this.toggle(); + }); + } + })); + } + + override dispose(): void { + this.statusBarEntries.forEach(entry => entry.dispose()); + this.deactivate(); + super.dispose(); + } +} diff --git a/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index a85e27af833..95e04e3c8ca 100644 --- a/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -41,7 +41,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { ResourceMap } from 'vs/base/common/map'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; import { AnythingQuickAccessProviderRunOptions, DefaultQuickAccessFilterValue, Extensions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; -import { EditorViewState, IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess'; +import { PickerEditorState, IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess'; import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ScrollType, IEditor } from 'vs/editor/common/editorCommon'; @@ -83,11 +83,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | undefined = undefined; - editorViewState: EditorViewState; + editorViewState = this._register(this.instantiationService.createInstance(PickerEditorState)); scorerCache: FuzzyScorerCache = Object.create(null); fileQueryCache: FileQueryCacheState | undefined = undefined; @@ -100,8 +100,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider): void { @@ -129,7 +132,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider; + let picks = new Array(); + if (options.additionPicks) { + picks.push(...options.additionPicks); + } if (this.pickState.isQuickNavigating) { + if (picks.length > 0) { + picks.push({ type: 'separator', label: localize('recentlyOpenedSeparator', "recently opened") } as IQuickPickSeparator); + } picks = historyEditorPicks; } else { - picks = []; if (options.includeHelp) { picks.push(...this.getHelpPicks(query, token, options)); } @@ -844,7 +860,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { - // disable and re-enable history service so that we can ignore this history entry - const disposable = this._historyService.suspendTracking(); - try { - await this._editorService.openEditor({ - resource: itemMatch.parent().resource, - options: { preserveFocus: true, revealIfOpened: true, ignoreError: true, selection: itemMatch.range() } - }); - } finally { - disposable.dispose(); - } + await this.editorViewState.openTransientEditor({ + resource: itemMatch.parent().resource, + options: { preserveFocus: true, revealIfOpened: true, ignoreError: true, selection: itemMatch.range() } + }); }); } })); - - disposables.add(Event.once(picker.onDidHide)(({ reason }) => { + disposables.add(Event.once(picker.onWillHide)(({ reason }) => { // Restore view state upon cancellation if we changed it // but only when the picker was closed via explicit user // gesture and not e.g. when focus was lost because that // could mean the user clicked into the editor directly. if (reason === QuickInputHideReason.Gesture) { - this.editorViewState.restore(true); + this.editorViewState.restore(); } + })); + + disposables.add(Event.once(picker.onDidHide)(({ reason }) => { this.searchModel.searchResult.toggleHighlights(false); })); @@ -225,11 +218,11 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider limit ? matches.slice(0, limit) : matches; - const picks: Array = []; + const picks: Array = []; for (let fileIndex = 0; fileIndex < matches.length; fileIndex++) { if (fileIndex === limit) { @@ -262,6 +255,10 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider => { + await this.handleAccept(fileMatch, {}); + return TriggerAction.CLOSE_PICKER; + }, }); const results: Match[] = fileMatch.matches() ?? []; diff --git a/code/src/vs/workbench/contrib/search/browser/search.contribution.ts b/code/src/vs/workbench/contrib/search/browser/search.contribution.ts index 0531e3a3776..5d2e48914c0 100644 --- a/code/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/code/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -308,6 +308,16 @@ configurationRegistry.registerConfiguration({ ], markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double-clicking a result in a search editor.") }, + 'search.searchEditor.singleClickBehaviour': { + type: 'string', + enum: ['default', 'peekDefinition',], + default: 'default', + enumDescriptions: [ + nls.localize('search.searchEditor.singleClickBehaviour.default', "Single-clicking does nothing."), + nls.localize('search.searchEditor.singleClickBehaviour.peekDefinition', "Single-clicking opens a Peek Definition window."), + ], + markdownDescription: nls.localize('search.searchEditor.singleClickBehaviour', "Configure effect of single-clicking a result in a search editor.") + }, 'search.searchEditor.reusePriorSearchConfiguration': { type: 'boolean', default: false, diff --git a/code/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/code/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index e7dbb128b7b..a253cc2738b 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -100,7 +100,7 @@ registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.SearchContext.HasSearchResults.negate(), Constants.SearchContext.ViewHasSomeCollapsibleKey)), }] }); @@ -122,7 +122,7 @@ registerAction2(class ExpandAllAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.SearchContext.HasSearchResults, Constants.SearchContext.ViewHasSomeCollapsibleKey.toNegated()), }] }); @@ -205,6 +205,7 @@ registerAction2(class ViewAsListAction extends Action2 { } }); + //#endregion //#region Helpers diff --git a/code/src/vs/workbench/contrib/search/browser/searchFindInput.ts b/code/src/vs/workbench/contrib/search/browser/searchFindInput.ts index 0d8db8e91d3..a220ba5e847 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchFindInput.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchFindInput.ts @@ -12,11 +12,21 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { NotebookFindInputFilterButton } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; import * as nls from 'vs/nls'; +import { IFindInputToggleOpts } from 'vs/base/browser/ui/findinput/findInputToggles'; +import { Codicon } from 'vs/base/common/codicons'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { Emitter } from 'vs/base/common/event'; + +const NLS_AI_TOGGLE_LABEL = nls.localize('aiDescription', "Use AI"); export class SearchFindInput extends ContextScopedFindInput { private _findFilter: NotebookFindInputFilterButton; + private _aiButton: AIToggle; private _filterChecked: boolean = false; private _visible: boolean = false; + private readonly _onDidChangeAIToggle = this._register(new Emitter()); + public readonly onDidChangeAIToggle = this._onDidChangeAIToggle.event; constructor( container: HTMLElement | null, @@ -26,6 +36,7 @@ export class SearchFindInput extends ContextScopedFindInput { readonly contextMenuService: IContextMenuService, readonly instantiationService: IInstantiationService, readonly filters: NotebookFindFilters, + private _shouldShowAIButton: boolean, // caller responsible for updating this when it changes, filterStartVisiblitity: boolean ) { super(container, contextViewProvider, options, contextKeyService); @@ -37,16 +48,51 @@ export class SearchFindInput extends ContextScopedFindInput { options, nls.localize('searchFindInputNotebookFilter.label', "Notebook Find Filters") )); - this.inputBox.paddingRight = (this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0) + this._findFilter.width; + + this._aiButton = this._register( + new AIToggle({ + appendTitle: '', + isChecked: false, + ...options.toggleStyles + })); + + this.setAdditionalToggles([this._aiButton]); + + + this.inputBox.paddingRight = (this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0) + this._findFilter.width + (this._aiButton?.width() ?? 0); + this.controls.appendChild(this._findFilter.container); this._findFilter.container.classList.add('monaco-custom-toggle'); - this.filterVisible = filterStartVisiblitity; + + this._register(this._aiButton.onChange(() => { + if (this._aiButton.checked) { + this.regex?.disable(); + this.wholeWords?.disable(); + this.caseSensitive?.disable(); + this._findFilter.disable(); + } else { + this.regex?.enable(); + this.wholeWords?.enable(); + this.caseSensitive?.enable(); + this._findFilter.enable(); + } + })); + + // ensure that ai button is visible if it should be + this._aiButton.domNode.style.display = _shouldShowAIButton ? '' : 'none'; } - set filterVisible(show: boolean) { - this._findFilter.container.style.display = show ? '' : 'none'; - this._visible = show; + set shouldShowAIButton(visible: boolean) { + if (this._shouldShowAIButton !== visible) { + this._shouldShowAIButton = visible; + this._aiButton.domNode.style.display = visible ? '' : 'none'; + } + } + + set filterVisible(visible: boolean) { + this._findFilter.container.style.display = visible ? '' : 'none'; + this._visible = visible; this.updateStyles(); } @@ -71,4 +117,22 @@ export class SearchFindInput extends ContextScopedFindInput { this._findFilter.applyStyles(this._filterChecked); } + + get isAIEnabled() { + return this._aiButton.checked; + } +} + +class AIToggle extends Toggle { + constructor(opts: IFindInputToggleOpts) { + super({ + icon: Codicon.sparkle, + title: NLS_AI_TOGGLE_LABEL + opts.appendTitle, + isChecked: opts.isChecked, + hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), + inputActiveOptionBorder: opts.inputActiveOptionBorder, + inputActiveOptionForeground: opts.inputActiveOptionForeground, + inputActiveOptionBackground: opts.inputActiveOptionBackground + }); + } } diff --git a/code/src/vs/workbench/contrib/search/browser/searchIcons.ts b/code/src/vs/workbench/contrib/search/browser/searchIcons.ts index f81dc87d394..066fbb8c836 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchIcons.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchIcons.ts @@ -29,3 +29,6 @@ export const searchViewIcon = registerIcon('search-view-icon', Codicon.search, l export const searchNewEditorIcon = registerIcon('search-new-editor', Codicon.newFile, localize('searchNewEditorIcon', 'Icon for the action to open a new search editor.')); export const searchOpenInFileIcon = registerIcon('search-open-in-file', Codicon.goToFile, localize('searchOpenInFile', 'Icon for the action to go to the file of the current search result.')); + +export const searchSparkleFilled = registerIcon('search-sparkle-filled', Codicon.sparkleFilled, localize('searchSparkleFilled', 'Icon to show AI results in search.')); +export const searchSparkleEmpty = registerIcon('search-sparkle-empty', Codicon.sparkle, localize('searchSparkleEmpty', 'Icon to hide AI results in search.')); diff --git a/code/src/vs/workbench/contrib/search/browser/searchModel.ts b/code/src/vs/workbench/contrib/search/browser/searchModel.ts index 0f280b7f1d0..e3f2a9167c6 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -26,7 +26,7 @@ import { IFileService, IFileStatWithPartialMetadata } from 'vs/platform/files/co import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; +import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { minimapFindMatch, overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; @@ -41,7 +41,7 @@ import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, I import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { rawCellPrefix, INotebookCellMatchNoModel, isINotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { IAITextQuery, IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; @@ -55,7 +55,7 @@ export class Match { // For replace private _fullPreviewRange: ISearchRange; - constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange) { + constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, public readonly aiContributed: boolean) { this._oneLinePreviewText = _fullPreviewLines[_fullPreviewRange.startLineNumber]; const adjustedEndCol = _fullPreviewRange.startLineNumber === _fullPreviewRange.endLineNumber ? _fullPreviewRange.endColumn : @@ -289,7 +289,7 @@ export class MatchInNotebook extends Match { private _webviewIndex: number | undefined; constructor(private readonly _cellParent: CellMatch, _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, webviewIndex?: number) { - super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange); + super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange, false); this._id = this._parent.id() + '>' + this._cellParent.cellIndex + (webviewIndex ? '_' + webviewIndex : '') + '_' + this.notebookMatchTypeString() + this._range + this.getMatchString(); this._webviewIndex = webviewIndex; } @@ -426,7 +426,6 @@ export class FileMatch extends Disposable implements IFileMatch { this._name = new Lazy(() => labelService.getUriBasenameLabel(this.resource)); this._cellMatches = new Map(); this._notebookUpdateScheduler = new RunOnceScheduler(this.updateMatchesForEditorWidget.bind(this), 250); - this.createMatches(); } addWebviewMatchesToCell(cellID: string, webviewMatches: ITextSearchMatch[]) { @@ -462,9 +461,10 @@ export class FileMatch extends Disposable implements IFileMatch { return this.matches().some(m => m instanceof MatchInNotebook && m.isReadonly()); } - createMatches(): void { + createMatches(isAiContributed: boolean): void { const model = this.modelService.getModel(this._resource); - if (model) { + if (model && !isAiContributed) { + // todo: handle better when ai contributed results has model, currently, createMatches does not work for this this.bindModel(model); this.updateMatchesForModel(); } else { @@ -477,7 +477,7 @@ export class FileMatch extends Disposable implements IFileMatch { this.rawMatch.results .filter(resultIsMatch) .forEach(rawMatch => { - textSearchResultToMatches(rawMatch, this) + textSearchResultToMatches(rawMatch, this, isAiContributed) .forEach(m => this.add(m)); }); } @@ -529,7 +529,7 @@ export class FileMatch extends Disposable implements IFileMatch { const matches = this._model .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, true, this._model); + this.updateMatches(matches, true, this._model, false); } @@ -549,17 +549,17 @@ export class FileMatch extends Disposable implements IFileMatch { const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, modelChange, this._model); + this.updateMatches(matches, modelChange, this._model, false); // await this.updateMatchesForEditorWidget(); } - private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel): void { + private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel, isAiContributed: boolean): void { const textSearchResults = editorMatchesToTextSearchResults(matches, model, this._previewOptions); textSearchResults.forEach(textSearchResult => { - textSearchResultToMatches(textSearchResult, this).forEach(match => { + textSearchResultToMatches(textSearchResult, this, isAiContributed).forEach(match => { if (!this._removedTextMatches.has(match.id())) { this.add(match); if (this.isMatchSelected(match)) { @@ -1142,7 +1142,7 @@ export class FolderMatch extends Disposable { return this._query; } - addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string): void { + addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string, isAiContributed: boolean): void { // when adding a fileMatch that has intermediate directories const added: FileMatch[] = []; const updated: FileMatch[] = []; @@ -1156,7 +1156,7 @@ export class FolderMatch extends Disposable { .results .filter(resultIsMatch) .forEach(m => { - textSearchResultToMatches(m, existingFileMatch) + textSearchResultToMatches(m, existingFileMatch, isAiContributed) .forEach(m => existingFileMatch.add(m)); }); } @@ -1350,7 +1350,7 @@ 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, + constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult, private readonly _ai: boolean, @IReplaceService replaceService: IReplaceService, @IInstantiationService instantiationService: IInstantiationService, @ILabelService labelService: ILabelService, @@ -1379,6 +1379,7 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { closestRoot, searchInstanceID ); + fileMatch.createMatches(this._ai); parent.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => parent.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); @@ -1441,6 +1442,7 @@ export class FolderMatchNoRoot extends FolderMatch { this, rawFileMatch, null, searchInstanceID)); + fileMatch.createMatches(false); // currently, no support for AI results in out-of-workspace files this.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => this.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); @@ -1588,8 +1590,10 @@ export class SearchResult extends Disposable { })); readonly onChange: Event = this._onChange.event; private _folderMatches: FolderMatchWorkspaceRoot[] = []; + private _aiFolderMatches: FolderMatchWorkspaceRoot[] = []; private _otherFilesMatch: FolderMatch | null = null; private _folderMatchesMap: TernarySearchTree = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); + private _aiFolderMatchesMap: TernarySearchTree = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); private _showHighlights: boolean = false; private _query: ITextQuery | null = null; private _rangeHighlightDecorations: RangeHighlightDecorations; @@ -1598,6 +1602,9 @@ export class SearchResult extends Disposable { private _onWillChangeModelListener: IDisposable | undefined; private _onDidChangeModelListener: IDisposable | undefined; + private _cachedSearchComplete: ISearchComplete | undefined; + private _aiCachedSearchComplete: ISearchComplete | undefined; + constructor( public readonly searchModel: SearchModel, @IReplaceService private readonly replaceService: IReplaceService, @@ -1619,7 +1626,7 @@ export class SearchResult extends Disposable { this._register(this.onChange(e => { if (e.removed) { - this._isDirty = !this.isEmpty(); + this._isDirty = !this.isEmpty() || !this.isEmpty(true); } })); } @@ -1683,8 +1690,12 @@ export class SearchResult extends Disposable { this._isDirty = false; }; + this._cachedSearchComplete = undefined; + this._aiCachedSearchComplete = undefined; + this._rangeHighlightDecorations.removeHighlightRange(); this._folderMatchesMap = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); + this._aiFolderMatchesMap = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); if (!query) { return; @@ -1692,14 +1703,33 @@ export class SearchResult extends Disposable { this._folderMatches = (query && query.folderQueries || []) .map(fq => fq.folder) - .map((resource, index) => this._createBaseFolderMatch(resource, resource.toString(), index, query)); + .map((resource, index) => this._createBaseFolderMatch(resource, resource.toString(), index, query, false)); this._folderMatches.forEach(fm => this._folderMatchesMap.set(fm.resource, fm)); - this._otherFilesMatch = this._createBaseFolderMatch(null, 'otherFiles', this._folderMatches.length + 1, query); + + this._aiFolderMatches = (query && query.folderQueries || []) + .map(fq => fq.folder) + .map((resource, index) => this._createBaseFolderMatch(resource, resource.toString(), index, query, true)); + + this._aiFolderMatches.forEach(fm => this._aiFolderMatchesMap.set(fm.resource, fm)); + + this._otherFilesMatch = this._createBaseFolderMatch(null, 'otherFiles', this._folderMatches.length + this._aiFolderMatches.length + 1, query, false); this._query = query; } + setCachedSearchComplete(cachedSearchComplete: ISearchComplete | undefined, ai: boolean) { + if (ai) { + this._aiCachedSearchComplete = cachedSearchComplete; + } else { + this._cachedSearchComplete = cachedSearchComplete; + } + } + + getCachedSearchComplete(ai: boolean) { + return ai ? this._aiCachedSearchComplete : this._cachedSearchComplete; + } + private onDidAddNotebookEditorWidget(widget: NotebookEditorWidget): void { this._onWillChangeModelListener?.dispose(); @@ -1737,10 +1767,10 @@ export class SearchResult extends Disposable { folderMatch?.unbindNotebookEditorWidget(editor, resource); } - private _createBaseFolderMatch(resource: URI | null, id: string, index: number, query: ITextQuery): FolderMatch { + private _createBaseFolderMatch(resource: URI | null, id: string, index: number, query: ITextQuery, ai: boolean): FolderMatch { let folderMatch: FolderMatch; if (resource) { - folderMatch = this._register(this.instantiationService.createInstance(FolderMatchWorkspaceRoot, resource, id, index, query, this)); + folderMatch = this._register(this.instantiationService.createInstance(FolderMatchWorkspaceRoot, resource, id, index, query, this, ai)); } else { folderMatch = this._register(this.instantiationService.createInstance(FolderMatchNoRoot, id, index, query, this)); } @@ -1750,31 +1780,36 @@ export class SearchResult extends Disposable { } - add(allRaw: IFileMatch[], searchInstanceID: string, silent: boolean = false): void { + add(allRaw: IFileMatch[], searchInstanceID: string, ai: boolean, silent: boolean = false): void { // Split up raw into a list per folder so we can do a batch add per folder. - const { byFolder, other } = this.groupFilesByFolder(allRaw); + const { byFolder, other } = this.groupFilesByFolder(allRaw, ai); byFolder.forEach(raw => { if (!raw.length) { return; } - const folderMatch = this.getFolderMatch(raw[0].resource); - folderMatch?.addFileMatch(raw, silent, searchInstanceID); + // ai results go into the respective folder + const folderMatch = ai ? this.getAIFolderMatch(raw[0].resource) : this.getFolderMatch(raw[0].resource); + folderMatch?.addFileMatch(raw, silent, searchInstanceID, ai); }); - this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID); + if (!ai) { + this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID, false); + } this.disposePastResults(); } clear(): void { this.folderMatches().forEach((folderMatch) => folderMatch.clear(true)); + this.folderMatches(true); this.disposeMatches(); this._folderMatches = []; + this._aiFolderMatches = []; this._otherFilesMatch = null; } - remove(matches: FileMatch | FolderMatch | (FileMatch | FolderMatch)[]): void { + remove(matches: FileMatch | FolderMatch | (FileMatch | FolderMatch)[], ai = false): void { if (!Array.isArray(matches)) { matches = [matches]; } @@ -1787,7 +1822,7 @@ export class SearchResult extends Disposable { const fileMatches: FileMatch[] = matches.filter(m => m instanceof FileMatch) as FileMatch[]; - const { byFolder, other } = this.groupFilesByFolder(fileMatches); + const { byFolder, other } = this.groupFilesByFolder(fileMatches, ai); byFolder.forEach(matches => { if (!matches.length) { return; @@ -1818,7 +1853,10 @@ export class SearchResult extends Disposable { }); } - folderMatches(): FolderMatch[] { + folderMatches(ai = false): FolderMatch[] { + if (ai) { + return this._aiFolderMatches; + } return this._otherFilesMatch ? [ ...this._folderMatches, @@ -1829,25 +1867,25 @@ export class SearchResult extends Disposable { ]; } - matches(): FileMatch[] { + matches(ai = false): FileMatch[] { const matches: FileMatch[][] = []; - this.folderMatches().forEach(folderMatch => { + this.folderMatches(ai).forEach(folderMatch => { matches.push(folderMatch.allDownstreamFileMatches()); }); return ([]).concat(...matches); } - isEmpty(): boolean { - return this.folderMatches().every((folderMatch) => folderMatch.isEmpty()); + isEmpty(ai = false): boolean { + return this.folderMatches(ai).every((folderMatch) => folderMatch.isEmpty()); } - fileCount(): number { - return this.folderMatches().reduce((prev, match) => prev + match.recursiveFileCount(), 0); + fileCount(ai = false): number { + return this.folderMatches(ai).reduce((prev, match) => prev + match.recursiveFileCount(), 0); } - count(): number { - return this.matches().reduce((prev, match) => prev + match.count(), 0); + count(ai = false): number { + return this.matches(ai).reduce((prev, match) => prev + match.count(), 0); } get showHighlights(): boolean { @@ -1887,19 +1925,24 @@ export class SearchResult extends Disposable { return folderMatch ? folderMatch : this._otherFilesMatch!; } + private getAIFolderMatch(resource: URI): FolderMatchWorkspaceRoot | FolderMatch | undefined { + const folderMatch = this._aiFolderMatchesMap.findSubstr(resource); + return folderMatch; + } + private set replacingAll(running: boolean) { this.folderMatches().forEach((folderMatch) => { folderMatch.replacingAll = running; }); } - private groupFilesByFolder(fileMatches: IFileMatch[]): { byFolder: ResourceMap; other: IFileMatch[] } { + private groupFilesByFolder(fileMatches: IFileMatch[], ai: boolean): { byFolder: ResourceMap; other: IFileMatch[] } { const rawPerFolder = new ResourceMap(); const otherFileMatches: IFileMatch[] = []; - this._folderMatches.forEach(fm => rawPerFolder.set(fm.resource, [])); + (ai ? this._aiFolderMatches : this._folderMatches).forEach(fm => rawPerFolder.set(fm.resource, [])); fileMatches.forEach(rawFileMatch => { - const folderMatch = this.getFolderMatch(rawFileMatch.resource); + const folderMatch = ai ? this.getAIFolderMatch(rawFileMatch.resource) : this.getFolderMatch(rawFileMatch.resource); if (!folderMatch) { // foldermatch was previously removed by user or disposed for some reason return; @@ -1921,8 +1964,14 @@ export class SearchResult extends Disposable { private disposeMatches(): void { this.folderMatches().forEach(folderMatch => folderMatch.dispose()); + this.folderMatches(true).forEach(folderMatch => folderMatch.dispose()); + this._folderMatches = []; + this._aiFolderMatches = []; + this._folderMatchesMap = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); + this._aiFolderMatchesMap = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); + this._rangeHighlightDecorations.removeHighlightRange(); } @@ -1951,6 +2000,7 @@ export class SearchModel extends Disposable { private _preserveCase: boolean = false; private _startStreamDelay: Promise = Promise.resolve(); private readonly _resultQueue: IFileMatch[] = []; + private readonly _aiResultQueue: IFileMatch[] = []; private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; @@ -1961,6 +2011,7 @@ export class SearchModel extends Disposable { readonly onSearchResultChanged: Event = this._onSearchResultChanged.event; private currentCancelTokenSource: CancellationTokenSource | null = null; + private currentAICancelTokenSource: CancellationTokenSource | null = null; private searchCancelledForNewSearch: boolean = false; public location: SearchModelLocation = SearchModelLocation.PANEL; @@ -1971,6 +2022,7 @@ export class SearchModel extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, @INotebookSearchService private readonly notebookSearchService: INotebookSearchService, + @IProgressService private readonly progressService: IProgressService, ) { super(); this._searchResult = this.instantiationService.createInstance(SearchResult, this); @@ -2013,6 +2065,57 @@ export class SearchModel extends Disposable { return this._searchResult; } + async addAIResults(onProgress?: (result: ISearchProgressItem) => void) { + if (this.searchResult.count(true)) { + // already has matches + return; + } else { + if (this._searchQuery) { + await this.aiSearch( + { ...this._searchQuery, contentPattern: this._searchQuery.contentPattern.pattern, type: QueryType.aiText }, + onProgress, + this.currentCancelTokenSource?.token, + ); + } + } + } + + private async doAISearchWithModal(searchQuery: IAITextQuery, searchInstanceID: string, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise { + const promise = this.searchService.aiTextSearch( + searchQuery, + token, async (p: ISearchProgressItem) => { + this.onSearchProgress(p, searchInstanceID, false, true); + onProgress?.(p); + }); + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + type: 'syncing', + title: 'Searching for AI results...', + }, async (_) => promise); + } + + aiSearch(query: IAITextQuery, onProgress?: (result: ISearchProgressItem) => void, callerToken?: CancellationToken): Promise { + + const searchInstanceID = Date.now().toString(); + const tokenSource = this.currentAICancelTokenSource = new CancellationTokenSource(callerToken); + const start = Date.now(); + const asyncAIResults = this.doAISearchWithModal(query, + searchInstanceID, + this.currentAICancelTokenSource.token, async (p: ISearchProgressItem) => { + this.onSearchProgress(p, searchInstanceID, false, true); + onProgress?.(p); + }) + .then( + value => { + this.onSearchCompleted(value, Date.now() - start, searchInstanceID, true); + return value; + }, + e => { + this.onSearchError(e, Date.now() - start, true); + throw e; + }).finally(() => tokenSource.dispose()); + return asyncAIResults; + } private doSearch(query: ITextQuery, progressEmitter: Emitter, searchQuery: ITextQuery, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void, callerToken?: CancellationToken): { asyncResults: Promise; @@ -2020,7 +2123,7 @@ export class SearchModel extends Disposable { } { const asyncGenerateOnProgress = async (p: ISearchProgressItem) => { progressEmitter.fire(); - this.onSearchProgress(p, searchInstanceID, false); + this.onSearchProgress(p, searchInstanceID, false, false); onProgress?.(p); }; @@ -2121,11 +2224,11 @@ export class SearchModel extends Disposable { return { asyncResults: asyncResults.then( value => { - this.onSearchCompleted(value, Date.now() - start, searchInstanceID); + this.onSearchCompleted(value, Date.now() - start, searchInstanceID, false); return value; }, e => { - this.onSearchError(e, Date.now() - start); + this.onSearchError(e, Date.now() - start, false); throw e; }), syncResults @@ -2141,13 +2244,20 @@ export class SearchModel extends Disposable { } } - private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string): ISearchComplete | undefined { + private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string, ai: boolean): ISearchComplete | undefined { if (!this._searchQuery) { throw new Error('onSearchCompleted must be called after a search is started'); } - this._searchResult.add(this._resultQueue, searchInstanceID); - this._resultQueue.length = 0; + if (ai) { + this._searchResult.add(this._aiResultQueue, searchInstanceID, true); + this._aiResultQueue.length = 0; + } else { + this._searchResult.add(this._resultQueue, searchInstanceID, false); + this._resultQueue.length = 0; + } + + this.searchResult.setCachedSearchComplete(completed, ai); const options: IPatternInfo = Object.assign({}, this._searchQuery.contentPattern); delete (options as any).pattern; @@ -2184,30 +2294,31 @@ export class SearchModel extends Disposable { return completed; } - private onSearchError(e: any, duration: number): void { + private onSearchError(e: any, duration: number, ai: boolean): void { if (errors.isCancellationError(e)) { this.onSearchCompleted( this.searchCancelledForNewSearch ? { exit: SearchCompletionExitCode.NewSearchStarted, results: [], messages: [] } : undefined, - duration, ''); + duration, '', ai); this.searchCancelledForNewSearch = false; } } - private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true) { + private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true, ai: boolean = false) { + const targetQueue = ai ? this._aiResultQueue : this._resultQueue; if ((p).resource) { - this._resultQueue.push(p); + targetQueue.push(p); if (sync) { - if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); - this._resultQueue.length = 0; + if (targetQueue.length) { + this._searchResult.add(targetQueue, searchInstanceID, false, true); + targetQueue.length = 0; } } else { this._startStreamDelay.then(() => { - if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); - this._resultQueue.length = 0; + if (targetQueue.length) { + this._searchResult.add(targetQueue, searchInstanceID, ai, true); + targetQueue.length = 0; } }); } @@ -2354,16 +2465,16 @@ export class RangeHighlightDecorations implements IDisposable { -function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch): Match[] { +function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch, isAiContributed: boolean): Match[] { const previewLines = rawMatch.preview.text.split('\n'); if (Array.isArray(rawMatch.ranges)) { return rawMatch.ranges.map((r, i) => { const previewRange: ISearchRange = (rawMatch.preview.matches)[i]; - return new Match(fileMatch, previewLines, previewRange, r); + return new Match(fileMatch, previewLines, previewRange, r, isAiContributed); }); } else { const previewRange = rawMatch.preview.matches; - const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges); + const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges, isAiContributed); return [match]; } } diff --git a/code/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/code/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 38a4f536403..e22115c2a8f 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -30,8 +30,8 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { SearchContext } from 'vs/workbench/contrib/search/common/constants'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; interface IFolderMatchTemplate { label: IResourceLabel; diff --git a/code/src/vs/workbench/contrib/search/browser/searchView.ts b/code/src/vs/workbench/contrib/search/browser/searchView.ts index 4a0a156fcf6..1b241efe943 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchView.ts @@ -75,14 +75,14 @@ import { createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/ import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; -import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from 'vs/workbench/services/search/common/search'; +import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, QueryType, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from 'vs/workbench/services/search/common/search'; import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = dom.$; @@ -125,6 +125,7 @@ export class SearchView extends ViewPane { private hasReplacePatternKey: IContextKey; private hasFilePatternKey: IContextKey; private hasSomeCollapsibleResultKey: IContextKey; + private hasAIResultProvider: IContextKey; private tree!: WorkbenchCompressibleObjectTree; private treeLabels!: ResourceLabels; @@ -158,6 +159,7 @@ export class SearchView extends ViewPane { private treeAccessibilityProvider: SearchAccessibilityProvider; private treeViewKey: IContextKey; + private aiResultsVisibleKey: IContextKey; private _visibleMatches: number = 0; @@ -218,6 +220,14 @@ export class SearchView extends ViewPane { this.hasFilePatternKey = Constants.SearchContext.ViewHasFilePatternKey.bindTo(this.contextKeyService); this.hasSomeCollapsibleResultKey = Constants.SearchContext.ViewHasSomeCollapsibleKey.bindTo(this.contextKeyService); this.treeViewKey = Constants.SearchContext.InTreeViewKey.bindTo(this.contextKeyService); + this.aiResultsVisibleKey = Constants.SearchContext.AIResultsVisibleKey.bindTo(this.contextKeyService); + this.hasAIResultProvider = Constants.SearchContext.hasAIResultProvider.bindTo(this.contextKeyService); + + this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(new Set(Constants.SearchContext.hasAIResultProvider.keys()))) { + this.refreshHasAISetting(); + } + })); // scoped this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container)); @@ -238,6 +248,8 @@ export class SearchView extends ViewPane { this.removeFileStats(); } this.refreshTree(); + } else if (e.affectsConfiguration('search.aiResults')) { + this.refreshHasAISetting(); } }); @@ -294,6 +306,14 @@ export class SearchView extends ViewPane { this.treeViewKey.set(visible); } + get aiResultsVisible(): boolean { + return this.aiResultsVisibleKey.get() ?? false; + } + + private set aiResultsVisible(visible: boolean) { + this.aiResultsVisibleKey.set(visible); + } + setTreeView(visible: boolean): void { if (visible === this.isTreeLayoutViewVisible) { return; @@ -303,6 +323,24 @@ export class SearchView extends ViewPane { this.refreshTree(); } + async setAIResultsVisible(visible: boolean): Promise { + if (visible === this.aiResultsVisible) { + return; + } + this.aiResultsVisible = visible; + if (this.viewModel.searchResult.isEmpty()) { + return; + } + + if (visible) { + await this.model.addAIResults(); + } else { + this.searchWidget.toggleReplace(false); + } + this.onSearchResultsChanged(); + this.onSearchComplete(() => { }, undefined, undefined, this.viewModel.searchResult.getCachedSearchComplete(visible)); + } + private get state(): SearchUIState { return this.searchStateKey.get() ?? SearchUIState.Idle; } @@ -323,6 +361,12 @@ export class SearchView extends ViewPane { return this.viewModel; } + private refreshHasAISetting() { + const val = this.shouldShowAIButton(); + if (val && this.searchWidget.searchInput) { + this.searchWidget.searchInput.shouldShowAIButton = val; + } + } private onDidChangeWorkbenchState(): void { if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.searchWithoutFolderMessageElement) { dom.hide(this.searchWithoutFolderMessageElement); @@ -376,8 +420,8 @@ export class SearchView extends ViewPane { }); const collapseResults = this.searchConfig.collapseResults; - if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches().length === 1) { - const onlyMatch = this.viewModel.searchResult.matches()[0]; + if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches(this.aiResultsVisible).length === 1) { + const onlyMatch = this.viewModel.searchResult.matches(this.aiResultsVisible)[0]; if (onlyMatch.count() < 50) { this.tree.expand(onlyMatch); } @@ -572,7 +616,8 @@ export class SearchView extends ViewPane { isInNotebookMarkdownPreview, isInNotebookCellInput, isInNotebookCellOutput, - } + }, + initialAIButtonVisibility: this.shouldShowAIButton() })); if (!this.searchWidget.searchInput || !this.searchWidget.replaceInput) { @@ -586,7 +631,14 @@ export class SearchView extends ViewPane { this._register(this.searchWidget.onSearchSubmit(options => this.triggerQueryChange(options))); this._register(this.searchWidget.onSearchCancel(({ focus }) => this.cancelSearch(focus))); - this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.triggerQueryChange())); + this._register(this.searchWidget.searchInput.onDidOptionChange(() => { + if (this.searchWidget.searchInput && this.searchWidget.searchInput.isAIEnabled !== this.aiResultsVisible) { + this.setAIResultsVisible(this.searchWidget.searchInput.isAIEnabled); + } else { + this.triggerQueryChange(); + } + })); + this._register(this.searchWidget.getNotebookFilters().onDidChange(() => this.triggerQueryChange())); const updateHasPatternKey = () => this.hasSearchPatternKey.set(this.searchWidget.searchInput ? (this.searchWidget.searchInput.getValue().length > 0) : false); @@ -625,7 +677,9 @@ export class SearchView extends ViewPane { this.trackInputBox(this.searchWidget.replaceInputFocusTracker); } - + private shouldShowAIButton(): boolean { + return !!(this.configurationService.getValue('search.aiResults') && this.hasAIResultProvider.get()); + } private onConfigurationUpdated(event?: IConfigurationChangeEvent): void { if (event && (event.affectsConfiguration('search.decorations.colors') || event.affectsConfiguration('search.decorations.badges'))) { this.refreshTree(); @@ -660,7 +714,7 @@ export class SearchView extends ViewPane { } private refreshAndUpdateCount(event?: IChangeEvent): void { - this.searchWidget.setReplaceAllActionState(!this.viewModel.searchResult.isEmpty()); + this.searchWidget.setReplaceAllActionState(!this.viewModel.searchResult.isEmpty(this.aiResultsVisible)); this.updateSearchResultCount(this.viewModel.searchResult.query!.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, event?.clearingAll); return this.refreshTree(event); } @@ -692,7 +746,7 @@ export class SearchView extends ViewPane { } private createResultIterator(collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable> { - const folderMatches = this.searchResult.folderMatches() + const folderMatches = this.searchResult.folderMatches(this.aiResultsVisible) .filter(fm => !fm.isEmpty()) .sort(searchMatchComparer); @@ -727,7 +781,11 @@ export class SearchView extends ViewPane { } private createFileIterator(fileMatch: FileMatch): Iterable> { - const matches = fileMatch.matches().sort(searchMatchComparer); + let matches = fileMatch.matches().sort(searchMatchComparer); + + if (!this.aiResultsVisible) { + matches = matches.filter(e => !e.aiContributed); + } return Iterable.map(matches, r => (>{ element: r, incompressible: true })); } @@ -1265,7 +1323,7 @@ export class SearchView extends ViewPane { } hasSearchResults(): boolean { - return !this.viewModel.searchResult.isEmpty(); + return !this.viewModel.searchResult.isEmpty(this.aiResultsVisible); } clearSearchResults(clearInput = true): void { @@ -1589,7 +1647,7 @@ export class SearchView extends ViewPane { } try { // Search result tree update - const fileCount = this.viewModel.searchResult.fileCount(); + const fileCount = this.viewModel.searchResult.fileCount(this.aiResultsVisible); if (this._visibleMatches !== fileCount) { this._visibleMatches = fileCount; this.refreshAndUpdateCount(); @@ -1611,14 +1669,14 @@ export class SearchView extends ViewPane { this.onSearchResultsChanged(); const collapseResults = this.searchConfig.collapseResults; - if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches().length === 1) { - const onlyMatch = this.viewModel.searchResult.matches()[0]; + if (collapseResults !== 'alwaysCollapse' && this.viewModel.searchResult.matches(this.aiResultsVisible).length === 1) { + const onlyMatch = this.viewModel.searchResult.matches(this.aiResultsVisible)[0]; if (onlyMatch.count() < 50) { this.tree.expand(onlyMatch); } } - const hasResults = !this.viewModel.searchResult.isEmpty(); + const hasResults = !this.viewModel.searchResult.isEmpty(this.aiResultsVisible); if (completed?.exit === SearchCompletionExitCode.NewSearchStarted) { return; } @@ -1686,7 +1744,7 @@ export class SearchView extends ViewPane { this.viewModel.searchResult.toggleHighlights(this.isVisible()); // show highlights // Indicate final search result count for ARIA - aria.status(nls.localize('ariaSearchResultsStatus', "Search returned {0} results in {1} files", this.viewModel.searchResult.count(), this.viewModel.searchResult.fileCount())); + aria.status(nls.localize('ariaSearchResultsStatus', "Search returned {0} results in {1} files", this.viewModel.searchResult.count(this.aiResultsVisible), this.viewModel.searchResult.fileCount())); } @@ -1741,6 +1799,20 @@ export class SearchView extends ViewPane { this.viewModel.replaceString = this.searchWidget.getReplaceValue(); const result = this.viewModel.search(query); + if (this.aiResultsVisible) { + const aiResult = this.viewModel.aiSearch({ ...query, contentPattern: query.contentPattern.pattern, type: QueryType.aiText }); + return result.asyncResults.then( + () => aiResult.then( + (complete) => { + clearTimeout(slowTimer); + this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete); + }, (e) => { + clearTimeout(slowTimer); + this.onSearchError(e, progressComplete, excludePatternText, includePatternText); + } + ) + ); + } return result.asyncResults.then((complete) => { clearTimeout(slowTimer); this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete); @@ -1785,13 +1857,14 @@ export class SearchView extends ViewPane { } private updateSearchResultCount(disregardExcludesAndIgnores?: boolean, onlyOpenEditors?: boolean, clear: boolean = false): void { - const fileCount = this.viewModel.searchResult.fileCount(); + const fileCount = this.viewModel.searchResult.fileCount(this.aiResultsVisible); + const resultCount = this.viewModel.searchResult.count(this.aiResultsVisible); this.hasSearchResultsKey.set(fileCount > 0); const msgWasHidden = this.messagesElement.style.display === 'none'; const messageEl = this.clearMessage(); - const resultMsg = clear ? '' : this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount); + const resultMsg = clear ? '' : this.buildResultCountMessage(resultCount, fileCount); this.tree.ariaLabel = resultMsg + nls.localize('forTerm', " - Search: {0}", this.searchResult.query?.contentPattern.pattern ?? ''); dom.append(messageEl, resultMsg); @@ -1987,7 +2060,13 @@ export class SearchView extends ViewPane { } // remove search results from this resource as it got disposed - const matches = this.viewModel.searchResult.matches(); + let matches = this.viewModel.searchResult.matches(); + for (let i = 0, len = matches.length; i < len; i++) { + if (resource.toString() === matches[i].resource.toString()) { + this.viewModel.searchResult.remove(matches[i]); + } + } + matches = this.viewModel.searchResult.matches(true); for (let i = 0, len = matches.length; i < len; i++) { if (resource.toString() === matches[i].resource.toString()) { this.viewModel.searchResult.remove(matches[i]); @@ -2108,7 +2187,7 @@ export class SearchView extends ViewPane { } private async retrieveFileStats(): Promise { - const files = this.searchResult.matches().filter(f => !f.fileStat).map(f => f.resolveFileStat(this.fileService)); + const files = this.searchResult.matches(this.aiResultsVisible).filter(f => !f.fileStat).map(f => f.resolveFileStat(this.fileService)); await Promise.all(files); } @@ -2121,6 +2200,9 @@ export class SearchView extends ViewPane { for (const fileMatch of this.searchResult.matches()) { fileMatch.fileStat = undefined; } + for (const fileMatch of this.searchResult.matches(true)) { + fileMatch.fileStat = undefined; + } } override dispose(): void { diff --git a/code/src/vs/workbench/contrib/search/browser/searchWidget.ts b/code/src/vs/workbench/contrib/search/browser/searchWidget.ts index 731653a4828..f244226b53f 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button, IButtonOptions } from 'vs/base/browser/ui/button/button'; -import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; +import { IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; import { IInputBoxStyles, IMessage, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; @@ -42,7 +42,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { GroupModelChangeKind } from 'vs/workbench/common/editor'; import { SearchFindInput } from 'vs/workbench/contrib/search/browser/searchFindInput'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; /** Specified in searchview.css */ const SingleLineInputHeight = 26; @@ -61,6 +61,7 @@ export interface ISearchWidgetOptions { inputBoxStyles: IInputBoxStyles; toggleStyles: IToggleStyles; notebookOptions?: NotebookToggleState; + initialAIButtonVisibility?: boolean; } interface NotebookToggleState { @@ -120,7 +121,7 @@ export class SearchWidget extends Widget { domNode: HTMLElement | undefined; - searchInput: FindInput | undefined; + searchInput: SearchFindInput | undefined; searchInputFocusTracker: dom.IFocusTracker | undefined; private searchInputBoxFocused: IContextKey; @@ -206,12 +207,12 @@ export class SearchWidget extends Widget { this._register( this._notebookFilters.onDidChange(() => { - if (this.searchInput instanceof SearchFindInput) { + if (this.searchInput) { this.searchInput.updateStyles(); } })); this._register(this.editorService.onDidEditorsChange((e) => { - if (this.searchInput instanceof SearchFindInput && + if (this.searchInput && e.event.editor instanceof NotebookEditorInput && (e.event.kind === GroupModelChangeKind.EDITOR_OPEN || e.event.kind === GroupModelChangeKind.EDITOR_CLOSE)) { this.searchInput.filterVisible = this._hasNotebookOpen(); @@ -402,7 +403,19 @@ export class SearchWidget extends Widget { const searchInputContainer = dom.append(parent, dom.$('.search-container.input-box')); - this.searchInput = this._register(new SearchFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService, this.contextMenuService, this.instantiationService, this._notebookFilters, this._hasNotebookOpen())); + this.searchInput = this._register( + new SearchFindInput( + searchInputContainer, + this.contextViewService, + inputOptions, + this.contextKeyService, + this.contextMenuService, + this.instantiationService, + this._notebookFilters, + options.initialAIButtonVisibility ?? false, + this._hasNotebookOpen() + ) + ); this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent)); this.searchInput.setValue(options.value || ''); diff --git a/code/src/vs/workbench/contrib/search/common/constants.ts b/code/src/vs/workbench/contrib/search/common/constants.ts index a8f1bc84311..a7f7c53b660 100644 --- a/code/src/vs/workbench/contrib/search/common/constants.ts +++ b/code/src/vs/workbench/contrib/search/common/constants.ts @@ -41,6 +41,8 @@ export const enum SearchCommandIds { ClearSearchResultsActionId = 'search.action.clearSearchResults', ViewAsTreeActionId = 'search.action.viewAsTree', ViewAsListActionId = 'search.action.viewAsList', + ShowAIResultsActionId = 'search.action.showAIResults', + HideAIResultsActionId = 'search.action.hideAIResults', ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails', ExcludeFolderFromSearchId = 'search.action.excludeFromSearch', FocusNextInputActionId = 'search.focus.nextInputBox', @@ -74,4 +76,6 @@ export const SearchContext = { ViewHasFilePatternKey: new RawContextKey('viewHasFilePattern', false), ViewHasSomeCollapsibleKey: new RawContextKey('viewHasSomeCollapsibleResult', false), InTreeViewKey: new RawContextKey('inTreeView', false), + AIResultsVisibleKey: new RawContextKey('AIResultsVisibleKey', false), + hasAIResultProvider: new RawContextKey('hasAIResultProviderKey', false), }; diff --git a/code/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/code/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 5e39773a0cc..9b3b9e4f4dd 100644 --- a/code/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/code/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -125,6 +125,7 @@ suite('Search Actions', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } @@ -145,7 +146,8 @@ suite('Search Actions', () => { startColumn: 0, endLineNumber: line, endColumn: 2 - } + }, + false ); fileMatch.add(match); return match; diff --git a/code/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts b/code/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts index 24e2448a9b3..17f9e88b338 100644 --- a/code/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts +++ b/code/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts @@ -14,7 +14,7 @@ import { ModelService } from 'vs/editor/common/services/modelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextQuery, ITextSearchMatch, OneLineRange, QueryType, TextSearchMatch } from 'vs/workbench/services/search/common/search'; +import { IAITextQuery, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextQuery, ITextSearchMatch, OneLineRange, QueryType, TextSearchMatch } from 'vs/workbench/services/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { CellMatch, MatchInNotebook, SearchModel } from 'vs/workbench/contrib/search/browser/searchModel'; @@ -122,6 +122,14 @@ suite('SearchModel', () => { }); }, + aiTextSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void, notebookURIs?: ResourceSet): Promise { + return new Promise(resolve => { + queueMicrotask(() => { + results.forEach(onProgress!); + resolve(complete!); + }); + }); + }, textSearchSplitSyncAsync(query: ITextQuery, token?: CancellationToken | undefined, onProgress?: ((result: ISearchProgressItem) => void) | undefined): { syncResults: ISearchComplete; asyncResults: Promise } { return { syncResults: { @@ -153,6 +161,11 @@ suite('SearchModel', () => { }); }); }, + aiTextSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void, notebookURIs?: ResourceSet): Promise { + return new Promise((resolve, reject) => { + reject(error); + }); + }, textSearchSplitSyncAsync(query: ITextQuery, token?: CancellationToken | undefined, onProgress?: ((result: ISearchProgressItem) => void) | undefined): { syncResults: ISearchComplete; asyncResults: Promise } { return { syncResults: { @@ -188,6 +201,17 @@ suite('SearchModel', () => { }); }); }, + aiTextSearch(query: IAITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void, notebookURIs?: ResourceSet): Promise { + const disposable = token?.onCancellationRequested(() => tokenSource.cancel()); + if (disposable) { + store.add(disposable); + } + + return Promise.resolve({ + results: [], + messages: [] + }); + }, textSearchSplitSyncAsync(query: ITextQuery, token?: CancellationToken | undefined, onProgress?: ((result: ISearchProgressItem) => void) | undefined): { syncResults: ISearchComplete; asyncResults: Promise } { const disposable = token?.onCancellationRequested(() => tokenSource.cancel()); if (disposable) { diff --git a/code/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/code/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index 0a591e21457..a90875c1384 100644 --- a/code/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/code/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -217,6 +217,7 @@ suite('searchNotebookHelpers', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(folderMatch); store.add(fileMatch); diff --git a/code/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/code/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index c6d94a0ebf3..e71fab4b131 100644 --- a/code/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/code/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -66,7 +66,7 @@ suite('SearchResult', () => { test('Line Match', function () { const fileMatch = aFileMatch('folder/file.txt', null!); - const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5)); + const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5), false); assert.strictEqual(lineMatch.text(), '0 foo bar'); assert.strictEqual(lineMatch.range().startLineNumber, 2); assert.strictEqual(lineMatch.range().endLineNumber, 2); @@ -174,7 +174,7 @@ suite('SearchResult', () => { const searchResult = instantiationService.createInstance(SearchResult, searchModel); store.add(searchResult); const fileMatch = aFileMatch('far/boo', searchResult); - const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3)); + const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3), false); assert(lineMatch.parent() === fileMatch); assert(fileMatch.parent() === searchResult.folderMatches()[0]); @@ -532,6 +532,7 @@ suite('SearchResult', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, root, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; diff --git a/code/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts b/code/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts index 5c5fcd10aab..b6e7dc04bbb 100644 --- a/code/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts +++ b/code/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts @@ -66,5 +66,5 @@ export function stubNotebookEditorService(instantiationService: TestInstantiatio } export function addToSearchResult(searchResult: SearchResult, allRaw: IFileMatch[], searchInstanceID = '') { - searchResult.add(allRaw, searchInstanceID); + searchResult.add(allRaw, searchInstanceID, false); } diff --git a/code/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/code/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 76b49f5f2bf..4feb3d343ed 100644 --- a/code/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/code/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -76,7 +76,7 @@ suite('Search - Viewlet', () => { endColumn: 1 } }] - }], ''); + }], '', false); const fileMatch = result.matches()[0]; const lineMatch = fileMatch.matches()[0]; @@ -89,9 +89,9 @@ suite('Search - Viewlet', () => { const fileMatch1 = aFileMatch('/foo'); const fileMatch2 = aFileMatch('/with/path'); const fileMatch3 = aFileMatch('/with/path/foo'); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); - const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); + const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); assert(searchMatchComparer(fileMatch2, fileMatch1) > 0); @@ -127,13 +127,13 @@ suite('Search - Viewlet', () => { const fileMatch2 = aFileMatch('/with/path.c', folderMatch2); const fileMatch3 = aFileMatch('/with/path/bar.b', folderMatch2); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); /*** * Structure would take the following form: @@ -180,6 +180,7 @@ suite('Search - Viewlet', () => { const fileMatch = instantiation.createInstance(FileMatch, { pattern: '' }, undefined, undefined, parentFolder ?? aFolderMatch('', 0), rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } diff --git a/code/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/code/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 02b833d6737..84bb2199629 100644 --- a/code/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/code/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -47,7 +47,7 @@ import { SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/s import { InSearchEditor, SearchEditorID, SearchEditorInputTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ITextQuery, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -62,8 +62,8 @@ import { UnusualLineTerminatorsDetector } from 'vs/editor/contrib/unusualLineTer import { defaultToggleStyles, getInputBoxStyle } from 'vs/platform/theme/browser/defaultStyles'; import { ILogService } from 'vs/platform/log/common/log'; import { SearchContext } from 'vs/workbench/contrib/search/common/constants'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const RESULT_LINE_REGEX = /^(\s+)(\d+)(: | )(\s*)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; @@ -97,6 +97,7 @@ export class SearchEditor extends AbstractTextCodeEditor private updatingModelForSearch: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -116,7 +117,7 @@ export class SearchEditor extends AbstractTextCodeEditor @IFileService fileService: IFileService, @ILogService private readonly logService: ILogService ) { - super(SearchEditor.ID, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); + super(SearchEditor.ID, group, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); this.container = DOM.$('.search-editor'); this.searchOperation = this._register(new LongRunningOperation(progressService)); @@ -248,7 +249,17 @@ export class SearchEditor extends AbstractTextCodeEditor private registerEditorListeners() { this.searchResultEditor.onMouseUp(e => { - if (e.event.detail === 2) { + if (e.event.detail === 1) { + const behaviour = this.searchConfig.searchEditor.singleClickBehaviour; + const position = e.target.position; + if (position && behaviour === 'peekDefinition') { + const line = this.searchResultEditor.getModel()?.getLineContent(position.lineNumber) ?? ''; + if (line.match(FILE_LINE_REGEX) || line.match(RESULT_LINE_REGEX)) { + this.searchResultEditor.setSelection(Range.fromPositions(position)); + this.commandService.executeCommand('editor.action.peekDefinition'); + } + } + } else if (e.event.detail === 2) { const behaviour = this.searchConfig.searchEditor.doubleClickBehaviour; const position = e.target.position; if (position && behaviour !== 'selectWord') { @@ -658,7 +669,7 @@ export class SearchEditor extends AbstractTextCodeEditor } private getInput(): SearchEditorInput | undefined { - return this._input as SearchEditorInput; + return this.input as SearchEditorInput; } private priorConfig: Partial> | undefined; diff --git a/code/src/vs/workbench/contrib/speech/browser/speech.contribution.ts b/code/src/vs/workbench/contrib/speech/browser/speech.contribution.ts index 03a6035fb80..7184018cd98 100644 --- a/code/src/vs/workbench/contrib/speech/browser/speech.contribution.ts +++ b/code/src/vs/workbench/contrib/speech/browser/speech.contribution.ts @@ -7,4 +7,4 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { ISpeechService } from 'vs/workbench/contrib/speech/common/speechService'; import { SpeechService } from 'vs/workbench/contrib/speech/browser/speechService'; -registerSingleton(ISpeechService, SpeechService, InstantiationType.Delayed); +registerSingleton(ISpeechService, SpeechService, InstantiationType.Eager /* Reads Extension Points */); diff --git a/code/src/vs/workbench/contrib/speech/browser/speechService.ts b/code/src/vs/workbench/contrib/speech/browser/speechService.ts index 00943f3262b..2a01f910efc 100644 --- a/code/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/code/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { firstOrDefault } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -11,23 +12,53 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILogService } from 'vs/platform/log/common/log'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { DeferredPromise } from 'vs/base/common/async'; -import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSession, SpeechToTextInProgress, IKeywordRecognitionSession, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSession, SpeechToTextInProgress, IKeywordRecognitionSession, KeywordRecognitionStatus, SpeechToTextStatus, speechLanguageConfigToLanguage, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export interface ISpeechProviderDescriptor { + readonly name: string; + readonly description?: string; +} + +const speechProvidersExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'speechProviders', + jsonSchema: { + description: localize('vscode.extension.contributes.speechProvider', 'Contributes a Speech Provider'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('speechProviderName', "Unique name for this Speech Provider."), + type: 'string' + }, + description: { + description: localize('speechProviderDescription', "A description of this Speech Provider, shown in the UI."), + type: 'string' + } + } + } + } +}); export class SpeechService extends Disposable implements ISpeechService { readonly _serviceBrand: undefined; - private readonly _onDidRegisterSpeechProvider = this._register(new Emitter()); - readonly onDidRegisterSpeechProvider = this._onDidRegisterSpeechProvider.event; + private readonly _onDidChangeHasSpeechProvider = this._register(new Emitter()); + readonly onDidChangeHasSpeechProvider = this._onDidChangeHasSpeechProvider.event; - private readonly _onDidUnregisterSpeechProvider = this._register(new Emitter()); - readonly onDidUnregisterSpeechProvider = this._onDidUnregisterSpeechProvider.event; - - get hasSpeechProvider() { return this.providers.size > 0; } + get hasSpeechProvider() { return this.providerDescriptors.size > 0 || this.providers.size > 0; } private readonly providers = new Map(); + private readonly providerDescriptors = new Map(); private readonly hasSpeechProviderContext = HasSpeechProvider.bindTo(this.contextKeyService); @@ -36,9 +67,35 @@ export class SpeechService extends Disposable implements ISpeechService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, + @IExtensionService private readonly extensionService: IExtensionService ) { super(); + + this.handleAndRegisterSpeechExtensions(); + } + + private handleAndRegisterSpeechExtensions(): void { + speechProvidersExtensionPoint.setHandler((extensions, delta) => { + const oldHasSpeechProvider = this.hasSpeechProvider; + + for (const extension of delta.removed) { + for (const descriptor of extension.value) { + this.providerDescriptors.delete(descriptor.name); + } + } + + for (const extension of delta.added) { + for (const descriptor of extension.value) { + this.providerDescriptors.set(descriptor.name, descriptor); + } + } + + if (oldHasSpeechProvider !== this.hasSpeechProvider) { + this.handleHasSpeechProviderChange(); + } + }); } registerSpeechProvider(identifier: string, provider: ISpeechProvider): IDisposable { @@ -46,21 +103,31 @@ export class SpeechService extends Disposable implements ISpeechService { throw new Error(`Speech provider with identifier ${identifier} is already registered.`); } + const oldHasSpeechProvider = this.hasSpeechProvider; + this.providers.set(identifier, provider); - this.hasSpeechProviderContext.set(true); - this._onDidRegisterSpeechProvider.fire(provider); + if (oldHasSpeechProvider !== this.hasSpeechProvider) { + this.handleHasSpeechProviderChange(); + } return toDisposable(() => { + const oldHasSpeechProvider = this.hasSpeechProvider; + this.providers.delete(identifier); - this._onDidUnregisterSpeechProvider.fire(provider); - if (this.providers.size === 0) { - this.hasSpeechProviderContext.set(false); + if (oldHasSpeechProvider !== this.hasSpeechProvider) { + this.handleHasSpeechProviderChange(); } }); } + private handleHasSpeechProviderChange(): void { + this.hasSpeechProviderContext.set(this.hasSpeechProvider); + + this._onDidChangeHasSpeechProvider.fire(); + } + private readonly _onDidStartSpeechToTextSession = this._register(new Emitter()); readonly onDidStartSpeechToTextSession = this._onDidStartSpeechToTextSession.event; @@ -72,15 +139,10 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly speechToTextInProgress = SpeechToTextInProgress.bindTo(this.contextKeyService); - createSpeechToTextSession(token: CancellationToken, context: string = 'speech'): ISpeechToTextSession { - const provider = firstOrDefault(Array.from(this.providers.values())); - if (!provider) { - throw new Error(`No Speech provider is registered.`); - } else if (this.providers.size > 1) { - this.logService.warn(`Multiple speech providers registered. Picking first one: ${provider.metadata.displayName}`); - } + async createSpeechToTextSession(token: CancellationToken, context: string = 'speech'): Promise { + const provider = await this.getProvider(); - const language = this.configurationService.getValue('accessibility.voice.speechLanguage'); + const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); const session = this._activeSpeechToTextSession = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); const sessionStart = Date.now(); @@ -92,24 +154,25 @@ export class SpeechService extends Disposable implements ISpeechService { if (session === this._activeSpeechToTextSession) { this._activeSpeechToTextSession = undefined; this.speechToTextInProgress.reset(); + this.accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStopped, { allowManyInParallel: true }); this._onDidEndSpeechToTextSession.fire(); type SpeechToTextSessionClassification = { owner: 'bpasero'; comment: 'An event that fires when a speech to text session is created'; - context: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Context of the session.' }; - duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Duration of the session.' }; - recognized: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If speech was recognized.' }; + context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; + sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Duration of the session.' }; + sessionRecognized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'If speech was recognized.' }; }; type SpeechToTextSessionEvent = { context: string; - duration: number; - recognized: boolean; + sessionDuration: number; + sessionRecognized: boolean; }; this.telemetryService.publicLog2('speechToTextSession', { context, - duration: Date.now() - sessionStart, - recognized: sessionRecognized + sessionDuration: Date.now() - sessionStart, + sessionRecognized }); } @@ -127,6 +190,7 @@ export class SpeechService extends Disposable implements ISpeechService { if (session === this._activeSpeechToTextSession) { this.speechToTextInProgress.set(true); this._onDidStartSpeechToTextSession.fire(); + this.accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStarted); } break; case SpeechToTextStatus.Recognizing: @@ -142,6 +206,21 @@ export class SpeechService extends Disposable implements ISpeechService { return session; } + private async getProvider(): Promise { + + // Send out extension activation to ensure providers can register + await this.extensionService.activateByEvent('onSpeech'); + + const provider = firstOrDefault(Array.from(this.providers.values())); + if (!provider) { + throw new Error(`No Speech provider is registered.`); + } else if (this.providers.size > 1) { + this.logService.warn(`Multiple speech providers registered. Picking first one: ${provider.metadata.displayName}`); + } + + return provider; + } + private readonly _onDidStartKeywordRecognition = this._register(new Emitter()); readonly onDidStartKeywordRecognition = this._onDidStartKeywordRecognition.event; @@ -154,6 +233,9 @@ export class SpeechService extends Disposable implements ISpeechService { async recognizeKeyword(token: CancellationToken): Promise { const result = new DeferredPromise(); + // Send out extension activation to ensure providers can register + await this.extensionService.activateByEvent('onSpeech'); + const disposables = new DisposableStore(); disposables.add(token.onCancellationRequested(() => { disposables.dispose(); @@ -201,25 +283,20 @@ export class SpeechService extends Disposable implements ISpeechService { type KeywordRecognitionClassification = { owner: 'bpasero'; comment: 'An event that fires when a speech keyword detection is started'; - recognized: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If the keyword was recognized.' }; + keywordRecognized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'If the keyword was recognized.' }; }; type KeywordRecognitionEvent = { - recognized: boolean; + keywordRecognized: boolean; }; this.telemetryService.publicLog2('keywordRecognition', { - recognized: status === KeywordRecognitionStatus.Recognized + keywordRecognized: status === KeywordRecognitionStatus.Recognized }); return status; } private async doRecognizeKeyword(token: CancellationToken): Promise { - const provider = firstOrDefault(Array.from(this.providers.values())); - if (!provider) { - throw new Error(`No Speech provider is registered.`); - } else if (this.providers.size > 1) { - this.logService.warn(`Multiple speech providers registered. Picking first one: ${provider.metadata.displayName}`); - } + const provider = await this.getProvider(); const session = this._activeKeywordRecognitionSession = provider.createKeywordRecognitionSession(token); this._onDidStartKeywordRecognition.fire(); diff --git a/code/src/vs/workbench/contrib/speech/common/speechService.ts b/code/src/vs/workbench/contrib/speech/common/speechService.ts index a594b4f3acf..6aced99f16e 100644 --- a/code/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/code/src/vs/workbench/contrib/speech/common/speechService.ts @@ -10,6 +10,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { language } from 'vs/base/common/platform'; export const ISpeechService = createDecorator('speechService'); @@ -67,8 +68,7 @@ export interface ISpeechService { readonly _serviceBrand: undefined; - readonly onDidRegisterSpeechProvider: Event; - readonly onDidUnregisterSpeechProvider: Event; + readonly onDidChangeHasSpeechProvider: Event; readonly hasSpeechProvider: boolean; @@ -83,7 +83,7 @@ export interface ISpeechService { * Starts to transcribe speech from the default microphone. The returned * session object provides an event to subscribe for transcribed text. */ - createSpeechToTextSession(token: CancellationToken, context?: string): ISpeechToTextSession; + createSpeechToTextSession(token: CancellationToken, context?: string): Promise; readonly onDidStartKeywordRecognition: Event; readonly onDidEndKeywordRecognition: Event; @@ -97,3 +97,105 @@ export interface ISpeechService { */ recognizeKeyword(token: CancellationToken): Promise; } + +export const SPEECH_LANGUAGE_CONFIG = 'accessibility.voice.speechLanguage'; + +export const SPEECH_LANGUAGES = { + ['da-DK']: { + name: localize('speechLanguage.da-DK', "Danish (Denmark)") + }, + ['de-DE']: { + name: localize('speechLanguage.de-DE', "German (Germany)") + }, + ['en-AU']: { + name: localize('speechLanguage.en-AU', "English (Australia)") + }, + ['en-CA']: { + name: localize('speechLanguage.en-CA', "English (Canada)") + }, + ['en-GB']: { + name: localize('speechLanguage.en-GB', "English (United Kingdom)") + }, + ['en-IE']: { + name: localize('speechLanguage.en-IE', "English (Ireland)") + }, + ['en-IN']: { + name: localize('speechLanguage.en-IN', "English (India)") + }, + ['en-NZ']: { + name: localize('speechLanguage.en-NZ', "English (New Zealand)") + }, + ['en-US']: { + name: localize('speechLanguage.en-US', "English (United States)") + }, + ['es-ES']: { + name: localize('speechLanguage.es-ES', "Spanish (Spain)") + }, + ['es-MX']: { + name: localize('speechLanguage.es-MX', "Spanish (Mexico)") + }, + ['fr-CA']: { + name: localize('speechLanguage.fr-CA', "French (Canada)") + }, + ['fr-FR']: { + name: localize('speechLanguage.fr-FR', "French (France)") + }, + ['hi-IN']: { + name: localize('speechLanguage.hi-IN', "Hindi (India)") + }, + ['it-IT']: { + name: localize('speechLanguage.it-IT', "Italian (Italy)") + }, + ['ja-JP']: { + name: localize('speechLanguage.ja-JP', "Japanese (Japan)") + }, + ['ko-KR']: { + name: localize('speechLanguage.ko-KR', "Korean (South Korea)") + }, + ['nl-NL']: { + name: localize('speechLanguage.nl-NL', "Dutch (Netherlands)") + }, + ['pt-PT']: { + name: localize('speechLanguage.pt-PT', "Portuguese (Portugal)") + }, + ['pt-BR']: { + name: localize('speechLanguage.pt-BR', "Portuguese (Brazil)") + }, + ['ru-RU']: { + name: localize('speechLanguage.ru-RU', "Russian (Russia)") + }, + ['sv-SE']: { + name: localize('speechLanguage.sv-SE', "Swedish (Sweden)") + }, + ['tr-TR']: { + // allow-any-unicode-next-line + name: localize('speechLanguage.tr-TR', "Turkish (Türkiye)") + }, + ['zh-CN']: { + name: localize('speechLanguage.zh-CN', "Chinese (Simplified, China)") + }, + ['zh-HK']: { + name: localize('speechLanguage.zh-HK', "Chinese (Traditional, Hong Kong)") + }, + ['zh-TW']: { + name: localize('speechLanguage.zh-TW', "Chinese (Traditional, Taiwan)") + } +}; + +export function speechLanguageConfigToLanguage(config: unknown, lang = language): string { + if (typeof config === 'string') { + if (config === 'auto') { + if (lang !== 'en') { + const langParts = lang.split('-'); + + return speechLanguageConfigToLanguage(`${langParts[0]}-${(langParts[1] ?? langParts[0]).toUpperCase()}`); + } + } else { + if (SPEECH_LANGUAGES[config as keyof typeof SPEECH_LANGUAGES]) { + return config; + } + } + } + + return 'en-US'; +} diff --git a/code/src/vs/workbench/contrib/speech/test/common/speechService.test.ts b/code/src/vs/workbench/contrib/speech/test/common/speechService.test.ts new file mode 100644 index 00000000000..d757eace7e0 --- /dev/null +++ b/code/src/vs/workbench/contrib/speech/test/common/speechService.test.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { speechLanguageConfigToLanguage } from 'vs/workbench/contrib/speech/common/speechService'; + +suite('SpeechService', () => { + + test('resolve language', async () => { + assert.strictEqual(speechLanguageConfigToLanguage(undefined), 'en-US'); + assert.strictEqual(speechLanguageConfigToLanguage(3), 'en-US'); + assert.strictEqual(speechLanguageConfigToLanguage('foo'), 'en-US'); + assert.strictEqual(speechLanguageConfigToLanguage('foo-bar'), 'en-US'); + + assert.strictEqual(speechLanguageConfigToLanguage('tr-TR'), 'tr-TR'); + assert.strictEqual(speechLanguageConfigToLanguage('zh-TW'), 'zh-TW'); + + assert.strictEqual(speechLanguageConfigToLanguage('auto', 'en'), 'en-US'); + assert.strictEqual(speechLanguageConfigToLanguage('auto', 'tr'), 'tr-TR'); + assert.strictEqual(speechLanguageConfigToLanguage('auto', 'zh-tw'), 'zh-TW'); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); diff --git a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 43246667cfe..5605c99c5d8 100644 --- a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1865,7 +1865,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer message: nls.localize('TaskSystem.saveBeforeRun.prompt.title', "Save all editors?"), detail: nls.localize('detail', "Do you want to save all editors before running the task?"), primaryButton: nls.localize({ key: 'saveBeforeRun.save', comment: ['&& denotes a mnemonic'] }, '&&Save'), - cancelButton: nls.localize('saveBeforeRun.dontSave', 'Don\'t save'), + cancelButton: nls.localize({ key: 'saveBeforeRun.dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"), }); if (!confirmed) { @@ -2417,6 +2417,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private async _computeTasksForSingleConfig(workspaceFolder: IWorkspaceFolder, config: TaskConfig.IExternalTaskRunnerConfiguration | undefined, runSource: TaskRunSource, custom: CustomTask[], customized: IStringDictionary, source: TaskConfig.TaskConfigSource, isRecentTask: boolean = false): Promise { if (!config) { return false; + } else if (!workspaceFolder) { + this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder for worskspace', this._workspace?.id); + return false; } const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); const problemReporter = new ProblemReporter(this._outputChannel); diff --git a/code/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/code/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index d07edd2679c..9f47ca8564e 100644 --- a/code/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/code/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -441,7 +441,7 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement }, 500, false, true)(async (markerEvent) => { markerChanged?.dispose(); markerChanged = undefined; - if (!markerEvent.includes(modelEvent.uri) || (this.markerService.read({ resource: modelEvent.uri }).length !== 0)) { + if (!markerEvent || !markerEvent.includes(modelEvent.uri) || (this.markerService.read({ resource: modelEvent.uri }).length !== 0)) { return; } const oldLines = Array.from(this.lines); diff --git a/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index a2d81dfbe73..d25127a5524 100644 --- a/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -250,7 +250,7 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc return { ...cur, affectedKeys: newAffectedKeys }; }, 1000, true); - debouncedConfigService(event => { + this._register(debouncedConfigService(event => { if (event.source !== ConfigurationTarget.DEFAULT) { type UpdateConfigurationClassification = { owner: 'sandy081'; @@ -267,7 +267,7 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc configurationKeys: Array.from(event.affectedKeys) }); } - }); + })); const { user, workspace } = configurationService.keys(); for (const setting of user) { @@ -282,12 +282,13 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc * Report value of a setting only if it is an enum, boolean, or number or an array of those. */ private getValueToReport(key: string, target: ConfigurationTarget.USER_LOCAL | ConfigurationTarget.WORKSPACE): string | undefined { - const schema = this.configurationRegistry.getConfigurationProperties()[key]; const inpsectData = this.configurationService.inspect(key); const value = target === ConfigurationTarget.USER_LOCAL ? inpsectData.user?.value : inpsectData.workspace?.value; if (isNumber(value) || isBoolean(value)) { return value.toString(); } + + const schema = this.configurationRegistry.getConfigurationProperties()[key]; if (isString(value)) { if (schema?.enum?.includes(value)) { return value; @@ -400,6 +401,15 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'source of the setting' }; }>('window.nativeTabs', { settingValue: this.getValueToReport(key, target), source }); return; + + case 'extensions.verifySignature': + this.telemetryService.publicLog2('extensions.verifySignature', { settingValue: this.getValueToReport(key, target), source }); + return; } } diff --git a/code/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts b/code/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts index 7710088cf64..12b5058d65a 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts @@ -46,7 +46,7 @@ export abstract class BaseTerminalBackend extends Disposable { this._register(this._ptyHostController.onPtyHostExit(() => { this._logService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`); })); - this.onPtyHostConnected(() => hasStarted = true); + this._register(this.onPtyHostConnected(() => hasStarted = true)); this._register(this._ptyHostController.onPtyHostStart(() => { this._logService.debug(`The terminal's pty host process is starting`); // Only fire the _restart_ event after it has started diff --git a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index e8c7ac8bada..406e5697c4e 100755 --- a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -50,7 +50,7 @@ if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" export $VARNAME="$VALUE" done builtin unset VSCODE_ENV_REPLACE @@ -59,7 +59,7 @@ if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" export $VARNAME="$VALUE${!VARNAME}" done builtin unset VSCODE_ENV_PREPEND @@ -68,7 +68,7 @@ if [ -n "${VSCODE_ENV_APPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" export $VARNAME="${!VARNAME}$VALUE" done builtin unset VSCODE_ENV_APPEND diff --git a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh index cc2cb83e0d2..d54b124e69a 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh +++ b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh @@ -32,12 +32,6 @@ if [[ "$VSCODE_INJECTION" == "1" ]]; then fi fi -# Shell integration was disabled by the shell, exit without warning assuming either the shell has -# explicitly disabled shell integration as it's incompatible or it implements the protocol. -if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then - builtin return -fi - # Apply EnvironmentVariableCollections if needed if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -rA ADDR <<< "$VSCODE_ENV_REPLACE" @@ -64,6 +58,12 @@ if [ -n "${VSCODE_ENV_APPEND:-}" ]; then unset VSCODE_ENV_APPEND fi +# Shell integration was disabled by the shell, exit without warning assuming either the shell has +# explicitly disabled shell integration as it's incompatible or it implements the protocol. +if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then + builtin return +fi + # The property (P) and command (E) codes embed values which require escaping. # Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex. __vsc_escape_value() { diff --git a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index 53e9bd30602..b38108ebd2e 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -24,7 +24,7 @@ $env:VSCODE_NONCE = $null if ($env:VSCODE_ENV_REPLACE) { $Split = $env:VSCODE_ENV_REPLACE.Split(":") foreach ($Item in $Split) { - $Inner = $Item.Split('=') + $Inner = $Item.Split('=', 2) [Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':')) } $env:VSCODE_ENV_REPLACE = $null @@ -32,7 +32,7 @@ if ($env:VSCODE_ENV_REPLACE) { if ($env:VSCODE_ENV_PREPEND) { $Split = $env:VSCODE_ENV_PREPEND.Split(":") foreach ($Item in $Split) { - $Inner = $Item.Split('=') + $Inner = $Item.Split('=', 2) [Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':') + [Environment]::GetEnvironmentVariable($Inner[0])) } $env:VSCODE_ENV_PREPEND = $null @@ -40,7 +40,7 @@ if ($env:VSCODE_ENV_PREPEND) { if ($env:VSCODE_ENV_APPEND) { $Split = $env:VSCODE_ENV_APPEND.Split(":") foreach ($Item in $Split) { - $Inner = $Item.Split('=') + $Inner = $Item.Split('=', 2) [Environment]::SetEnvironmentVariable($Inner[0], [Environment]::GetEnvironmentVariable($Inner[0]) + $Inner[1].Replace('\x3a', ':')) } $env:VSCODE_ENV_APPEND = $null diff --git a/code/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/code/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 488815cf258..e30f5dd92d9 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/code/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -474,6 +474,11 @@ pointer-events: none; } +.terminal-range-highlight { + outline: 1px solid var(--vscode-focusBorder); + pointer-events: none; +} + .terminal-command-guide { left: 0; border: 1.5px solid #ffffff; diff --git a/code/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts b/code/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts index 524a251532a..7841d76d329 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts @@ -32,6 +32,8 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statusbar'; export class RemoteTerminalBackendContribution implements IWorkbenchContribution { + static ID = 'remoteTerminalBackend'; + constructor( @IInstantiationService instantiationService: IInstantiationService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/code/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 19b23b74fd4..b0ed0ec100f 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -4,57 +4,56 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Schemas } from 'vs/base/common/network'; +import { isIOS, isWindows } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/scrollbar'; -import 'vs/css!./media/widgets'; -import 'vs/css!./media/xterm'; import 'vs/css!./media/terminal'; import 'vs/css!./media/terminalVoice'; +import 'vs/css!./media/widgets'; +import 'vs/css!./media/xterm'; import * as nls from 'vs/nls'; -import { URI } from 'vs/base/common/uri'; +import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingWeight, KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; -import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; import { Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, IDraggedResourceEditorInput } from 'vs/platform/dnd/browser/dnd'; -import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; -import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { TERMINAL_VIEW_ID, TerminalCommandId, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; -import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; -import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; -import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; -import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, TerminalDataTransfers, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IKeybindings, KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; -import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; -import { registerTerminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; -import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; -import { terminalViewIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; +import { Registry } from 'vs/platform/registry/common/platform'; import { ITerminalLogService, TerminalSettingId, WindowsShellType } from 'vs/platform/terminal/common/terminal'; -import { isIOS, isWindows } from 'vs/base/common/platform'; -import { setupTerminalMenus } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; -import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminalInstanceService'; +import { TerminalLogService } from 'vs/platform/terminal/common/terminalLogService'; import { registerTerminalPlatformConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; -import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; +import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { IViewContainersRegistry, IViewsRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation } from 'vs/workbench/common/views'; +import { RemoteTerminalBackendContribution } from 'vs/workbench/contrib/terminal/browser/remoteTerminalBackend'; +import { ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, TerminalDataTransfers, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; -import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; -import { TerminalEditorService } from 'vs/workbench/contrib/terminal/browser/terminalEditorService'; import { TerminalInputSerializer } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; +import { TerminalEditorService } from 'vs/workbench/contrib/terminal/browser/terminalEditorService'; import { TerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminalGroupService'; -import { TerminalContextKeys, TerminalContextKeyStrings } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { TerminalProfileService } from 'vs/workbench/contrib/terminal/browser/terminalProfileService'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { RemoteTerminalBackendContribution } from 'vs/workbench/contrib/terminal/browser/remoteTerminalBackend'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { terminalViewIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; +import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminalInstanceService'; import { TerminalMainContribution } from 'vs/workbench/contrib/terminal/browser/terminalMainContribution'; -import { Schemas } from 'vs/base/common/network'; +import { setupTerminalMenus } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; +import { TerminalProfileService } from 'vs/workbench/contrib/terminal/browser/terminalProfileService'; +import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; +import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; +import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; +import { ITerminalProfileService, TERMINAL_VIEW_ID, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { registerTerminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; +import { TerminalContextKeyStrings, TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { setupCheTerminalMenus } from 'vs/workbench/contrib/terminal/browser/che/terminalMenus'; -import { TerminalLogService } from 'vs/platform/terminal/common/terminalLogService'; // Register services registerSingleton(ITerminalLogService, TerminalLogService, InstantiationType.Delayed); @@ -80,9 +79,9 @@ const quickAccessNavigatePreviousInTerminalPickerId = 'workbench.action.quickOpe CommandsRegistry.registerCommand({ id: quickAccessNavigatePreviousInTerminalPickerId, handler: getQuickNavigateHandler(quickAccessNavigatePreviousInTerminalPickerId, false) }); // Register workbench contributions -const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchRegistry.registerWorkbenchContribution(TerminalMainContribution, LifecyclePhase.Restored); -workbenchRegistry.registerWorkbenchContribution(RemoteTerminalBackendContribution, LifecyclePhase.Restored); +// This contribution blocks startup as it's critical to enable the web embedder window.createTerminal API +registerWorkbenchContribution2(TerminalMainContribution.ID, TerminalMainContribution, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(RemoteTerminalBackendContribution.ID, RemoteTerminalBackendContribution, WorkbenchPhase.AfterRestored); // Register configurations registerTerminalPlatformConfiguration(); diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminal.ts b/code/src/vs/workbench/contrib/terminal/browser/terminal.ts index 9a1bd1ab1b7..229e1b0b124 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -23,11 +23,12 @@ import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/termi import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfiguration, ITerminalFont, ITerminalProcessExtHostProxy, ITerminalProcessInfo } from 'vs/workbench/contrib/terminal/common/terminal'; import { ISimpleSelectedSuggestion } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget'; -import type { IMarker, ITheme, Terminal as RawXtermTerminal } from '@xterm/xterm'; +import type { IMarker, ITheme, Terminal as RawXtermTerminal, IBufferRange } from '@xterm/xterm'; import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import type { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalEditorService = createDecorator('terminalEditorService'); @@ -113,13 +114,17 @@ export interface IMarkTracker { selectToNextMark(): void; selectToPreviousLine(): void; selectToNextLine(): void; - clearMarker(): void; + clear(): void; scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void; scrollToLine(line: number, position: ScrollPosition): void; - revealCommand(command: ITerminalCommand, position?: ScrollPosition): void; + revealCommand(command: ITerminalCommand | ICurrentPartialCommand, position?: ScrollPosition): void; + revealRange(range: IBufferRange): void; registerTemporaryDecoration(marker: IMarker, endMarker: IMarker | undefined, showOutline: boolean): void; showCommandGuide(command: ITerminalCommand | undefined): void; + + saveScrollState(): void; + restoreScrollState(): void; } export interface ITerminalGroup { diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 50a7fc56a29..9e6c751c89c 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1654,7 +1654,7 @@ export function registerTerminalActions() { registerActiveInstanceAction({ id: TerminalCommandId.StartVoice, - title: localize2('workbench.action.terminal.startVoice', "Start Terminal Voice"), + title: localize2('workbench.action.terminal.startDictation', "Start Dictation in Terminal"), precondition: ContextKeyExpr.and(HasSpeechProvider, sharedWhenClause.terminalAvailable), f1: true, run: (activeInstance, c, accessor) => { @@ -1665,7 +1665,7 @@ export function registerTerminalActions() { registerActiveInstanceAction({ id: TerminalCommandId.StopVoice, - title: localize2('workbench.action.terminal.stopVoice', "Stop Terminal Voice"), + title: localize2('workbench.action.terminal.stopDictation', "Stop Dictation in Terminal"), precondition: ContextKeyExpr.and(HasSpeechProvider, sharedWhenClause.terminalAvailable), f1: true, run: (activeInstance, c, accessor) => { diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts new file mode 100644 index 00000000000..86677966a9b --- /dev/null +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is a one-off/safe import, to expose to outside contfibs as in general we don't want them +// to touch terminalContrib either. +// eslint-disable-next-line local/code-import-patterns +export { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 6835b5e9c35..900fbaf9fed 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -47,6 +47,7 @@ export class TerminalEditor extends EditorPane { private _cancelContextMenu: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -61,7 +62,7 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService ) { - super(terminalEditorId, telemetryService, themeService, storageService); + super(terminalEditorId, group, telemetryService, themeService, storageService); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, contextKeyService)); this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalInstanceContext, contextKeyService)); } @@ -74,7 +75,7 @@ export class TerminalEditor extends EditorPane { if (this._lastDimension) { this.layout(this._lastDimension); } - this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); if (this._editorInput.terminalInstance) { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set @@ -102,7 +103,7 @@ export class TerminalEditor extends EditorPane { override focus() { super.focus(); - this._editorInput?.terminalInstance?.focus(); + this._editorInput?.terminalInstance?.focus(true); } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -143,7 +144,7 @@ export class TerminalEditor extends EditorPane { // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); return; } @@ -181,7 +182,7 @@ export class TerminalEditor extends EditorPane { else if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') { if (!this._cancelContextMenu) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); } event.preventDefault(); event.stopImmediatePropagation(); @@ -199,9 +200,9 @@ export class TerminalEditor extends EditorPane { this._lastDimension = dimension; } - override setVisible(visible: boolean, group?: IEditorGroup): void { - super.setVisible(visible, group); - this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + override setVisible(visible: boolean): void { + super.setVisible(visible); + this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); } override getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined { diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalEvents.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalEvents.ts index 9f1630864a8..dc1d7a2c515 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalEvents.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalEvents.ts @@ -33,30 +33,30 @@ export function createInstanceCapabilityEventMultiplexer Event.map(instance.capabilities.onDidAddCapability, changeEvent => ({ instance, changeEvent })) - ); - addCapabilityMultiplexer.event(e => { + )); + store.add(addCapabilityMultiplexer.event(e => { if (e.changeEvent.id === capabilityId) { addCapability(e.instance, e.changeEvent.capability); } - }); + })); // Removed capabilities - const removeCapabilityMultiplexer = new DynamicListEventMultiplexer( + const removeCapabilityMultiplexer = store.add(new DynamicListEventMultiplexer( currentInstances, onAddInstance, onRemoveInstance, instance => instance.capabilities.onDidRemoveCapability - ); - removeCapabilityMultiplexer.event(e => { + )); + store.add(removeCapabilityMultiplexer.event(e => { if (e.id === capabilityId) { capabilityListeners.deleteAndDispose(e.capability); } - }); + })); return { dispose: () => store.dispose(), diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index d993243770f..171c69d6ed3 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -7,7 +7,7 @@ import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal' import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, Disposable, DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { SplitView, Orientation, IView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; -import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITerminalInstance, Direction, ITerminalGroup, ITerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; @@ -42,7 +42,6 @@ class SplitPaneContainer extends Disposable { constructor( private _container: HTMLElement, public orientation: Orientation, - @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService ) { super(); this._width = this._container.offsetWidth; @@ -61,25 +60,7 @@ class SplitPaneContainer extends Disposable { this._addChild(instance, index); } - resizePane(index: number, direction: Direction, amount: number, part: Parts): void { - const isHorizontal = (direction === Direction.Left) || (direction === Direction.Right); - - if ((isHorizontal && this.orientation !== Orientation.HORIZONTAL) || - (!isHorizontal && this.orientation !== Orientation.VERTICAL)) { - // Resize the entire pane as a whole - if ( - (this.orientation === Orientation.HORIZONTAL && direction === Direction.Down) || - (part === Parts.SIDEBAR_PART && direction === Direction.Left) || - (part === Parts.AUXILIARYBAR_PART && direction === Direction.Right) - ) { - amount *= -1; - } - - this._layoutService.resizePart(part, amount, amount); - return; - } - - // Resize left/right in horizontal or up/down in vertical + resizePane(index: number, direction: Direction, amount: number): void { // Only resize when there is more than one pane if (this._children.length <= 1) { return; @@ -570,17 +551,57 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this.setActiveInstanceByIndex(newIndex); } + private _getPosition(): Position { + switch (this._terminalLocation) { + case ViewContainerLocation.Panel: + return this._panelPosition; + case ViewContainerLocation.Sidebar: + return this._layoutService.getSideBarPosition(); + case ViewContainerLocation.AuxiliaryBar: + return this._layoutService.getSideBarPosition() === Position.LEFT ? Position.RIGHT : Position.LEFT; + } + } + + private _getOrientation(): Orientation { + return this._getPosition() === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + } + resizePane(direction: Direction): void { if (!this._splitPaneContainer) { return; } - const isHorizontal = (direction === Direction.Left || direction === Direction.Right); + const isHorizontalResize = (direction === Direction.Left || direction === Direction.Right); + + const groupOrientation = this._getOrientation(); + + const shouldResizePart = + (isHorizontalResize && groupOrientation === Orientation.VERTICAL) || + (!isHorizontalResize && groupOrientation === Orientation.HORIZONTAL); + const font = this._terminalService.configHelper.getFont(getWindow(this._groupElement)); // TODO: Support letter spacing and line height - const charSize = (isHorizontal ? font.charWidth : font.charHeight); + const charSize = (isHorizontalResize ? font.charWidth : font.charHeight); + if (charSize) { - this._splitPaneContainer.resizePane(this._activeInstanceIndex, direction, charSize * Constants.ResizePartCellCount, getPartByLocation(this._terminalLocation)); + let resizeAmount = charSize * Constants.ResizePartCellCount; + + if (shouldResizePart) { + + const shouldShrink = + (this._getPosition() === Position.LEFT && direction === Direction.Left) || + (this._getPosition() === Position.RIGHT && direction === Direction.Right) || + (this._getPosition() === Position.BOTTOM && direction === Direction.Down); + + if (shouldShrink) { + resizeAmount *= -1; + } + + this._layoutService.resizePart(getPartByLocation(this._terminalLocation), resizeAmount, resizeAmount); + } else { + this._splitPaneContainer.resizePane(this._activeInstanceIndex, direction, resizeAmount); + } + } } diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index b177b4fad02..caa2bbd3fa5 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -66,13 +66,11 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe ) { super(); - this.onDidDisposeGroup(group => this._removeGroup(group)); - this._terminalGroupCountContextKey = TerminalContextKeys.groupCount.bindTo(this._contextKeyService); - this.onDidChangeGroups(() => this._terminalGroupCountContextKey.set(this.groups.length)); - - Event.any(this.onDidChangeActiveGroup, this.onDidChangeInstances)(() => this.updateVisibility()); + this._register(this.onDidDisposeGroup(group => this._removeGroup(group))); + this._register(this.onDidChangeGroups(() => this._terminalGroupCountContextKey.set(this.groups.length))); + this._register(Event.any(this.onDidChangeActiveGroup, this.onDidChangeInstances)(() => this.updateVisibility())); } hidePanel(): void { diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index bdb75889fa5..ef5287bd920 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -733,7 +733,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { throw new ErrorNoTelemetry('Terminal disposed of during xterm.js creation'); } - const disableShellIntegrationReporting = (this.shellLaunchConfig.hideFromUser || this.shellLaunchConfig.executable === undefined || this.shellType === undefined) || !shellIntegrationSupportedShellTypes.includes(this.shellType); + const disableShellIntegrationReporting = (this.shellLaunchConfig.executable === undefined || this.shellType === undefined) || !shellIntegrationSupportedShellTypes.includes(this.shellType); const xterm = this._scopedInstantiationService.createInstance( XtermTerminal, Terminal, @@ -1471,23 +1471,22 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private _onProcessData(ev: IProcessDataEvent): void { - const messageId = ++this._latestXtermWriteData; if (ev.trackCommit) { - ev.writePromise = new Promise(r => { - this.xterm?.raw.write(ev.data, () => { - this._latestXtermParseData = messageId; - this._processManager.acknowledgeDataEvent(ev.data.length); - r(); - }); - }); + ev.writePromise = new Promise(r => this._writeProcessData(ev, r)); } else { - this.xterm?.raw.write(ev.data, () => { - this._latestXtermParseData = messageId; - this._processManager.acknowledgeDataEvent(ev.data.length); - }); + this._writeProcessData(ev); } } + private _writeProcessData(ev: IProcessDataEvent, cb?: () => void) { + const messageId = ++this._latestXtermWriteData; + this.xterm?.raw.write(ev.data, () => { + this._latestXtermParseData = messageId; + this._processManager.acknowledgeDataEvent(ev.data.length); + cb?.(); + }); + } + /** * Called when either a process tied to a terminal has exited or when a terminal renderer * simulates a process exiting (e.g. custom execution task). diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts index 69fe49e69da..2ff5e51cfe6 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts @@ -12,6 +12,8 @@ import { ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService import { parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IEmbedderTerminalService } from 'vs/workbench/services/terminal/common/embedderTerminalService'; /** @@ -20,10 +22,14 @@ import { IEmbedderTerminalService } from 'vs/workbench/services/terminal/common/ * be more relevant). */ export class TerminalMainContribution extends Disposable implements IWorkbenchContribution { + static ID = 'terminalMain'; + constructor( @IEditorResolverService editorResolverService: IEditorResolverService, @IEmbedderTerminalService embedderTerminalService: IEmbedderTerminalService, + @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, @ILabelService labelService: ILabelService, + @ILifecycleService lifecycleService: ILifecycleService, @ITerminalService terminalService: ITerminalService, @ITerminalEditorService terminalEditorService: ITerminalEditorService, @ITerminalGroupService terminalGroupService: ITerminalGroupService, @@ -31,8 +37,50 @@ export class TerminalMainContribution extends Disposable implements IWorkbenchCo ) { super(); + this._init( + editorResolverService, + embedderTerminalService, + workbenchEnvironmentService, + labelService, + lifecycleService, + terminalService, + terminalEditorService, + terminalGroupService, + terminalInstanceService + ); + } + + private async _init( + editorResolverService: IEditorResolverService, + embedderTerminalService: IEmbedderTerminalService, + workbenchEnvironmentService: IWorkbenchEnvironmentService, + labelService: ILabelService, + lifecycleService: ILifecycleService, + terminalService: ITerminalService, + terminalEditorService: ITerminalEditorService, + terminalGroupService: ITerminalGroupService, + terminalInstanceService: ITerminalInstanceService + ) { + // Defer this for the local case only. This is important for the + // window.createTerminal web embedder API to work before the workbench + // is loaded on remote + if (workbenchEnvironmentService.remoteAuthority === undefined) { + await lifecycleService.when(LifecyclePhase.Restored); + } + + this._register(embedderTerminalService.onDidCreateTerminal(async embedderTerminal => { + const terminal = await terminalService.createTerminal({ + config: embedderTerminal, + location: TerminalLocation.Panel + }); + terminalService.setActiveInstance(terminal); + await terminalService.revealActiveTerminal(); + })); + + await lifecycleService.when(LifecyclePhase.Restored); + // Register terminal editors - editorResolverService.registerEditor( + this._register(editorResolverService.registerEditor( `${Schemas.vscodeTerminal}:/**`, { id: terminalEditorId, @@ -80,24 +128,15 @@ export class TerminalMainContribution extends Disposable implements IWorkbenchCo }; } } - ); + )); // Register a resource formatter for terminal URIs - labelService.registerFormatter({ + this._register(labelService.registerFormatter({ scheme: Schemas.vscodeTerminal, formatting: { label: '${path}', separator: '' } - }); - - embedderTerminalService.onDidCreateTerminal(async embedderTerminal => { - const terminal = await terminalService.createTerminal({ - config: embedderTerminal, - location: TerminalLocation.Panel - }); - terminalService.setActiveInstance(terminal); - await terminalService.revealActiveTerminal(); - }); + })); } } diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 7f119914b09..a000c5e8465 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -20,17 +20,17 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed const enum ContextMenuGroup { Create = '1_create', - Edit = '2_edit', - Clear = '3_clear', - Kill = '4_kill', - Config = '5_config' + Edit = '3_edit', + Clear = '5_clear', + Kill = '7_kill', + Config = '9_config' } export const enum TerminalMenuBarGroup { Create = '1_create', - Run = '2_run', - Manage = '3_manage', - Configure = '4_configure' + Run = '3_run', + Manage = '5_manage', + Configure = '7_configure' } export function setupTerminalMenus(): void { diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 81770243f91..b731bf456fd 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -40,6 +40,7 @@ import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } import { generateUuid } from 'vs/base/common/uuid'; import { getActiveWindow, runWhenWindowIdle } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; const enum ProcessConstants { /** @@ -436,7 +437,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); - if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (!this._isDisposed && shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index 63dafffca62..87a2bb83a65 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -89,7 +89,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi private async _setupConfigListener(): Promise { const platformKey = await this.getPlatformKey(); - this._configurationService.onDidChangeConfiguration(async e => { + this._register(this._configurationService.onDidChangeConfiguration(async e => { if (e.affectsConfiguration(TerminalSettingPrefix.AutomationProfile + platformKey) || e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + platformKey) || e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) || @@ -103,7 +103,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi this._platformConfigJustRefreshed = true; } } - }); + })); } getDefaultProfileName(): string | undefined { diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts index f78008e14db..22d6515637e 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -190,19 +190,19 @@ export class TerminalService extends Disposable implements ITerminalService { // the below avoids having to poll routinely. // we update detected profiles when an instance is created so that, // for example, we detect if you've installed a pwsh - this.onDidCreateInstance(() => this._terminalProfileService.refreshAvailableProfiles()); + this._register(this.onDidCreateInstance(() => this._terminalProfileService.refreshAvailableProfiles())); this._forwardInstanceHostEvents(this._terminalGroupService); this._forwardInstanceHostEvents(this._terminalEditorService); - this._terminalGroupService.onDidChangeActiveGroup(this._onDidChangeActiveGroup.fire, this._onDidChangeActiveGroup); - this._terminalInstanceService.onDidCreateInstance(instance => { + this._register(this._terminalGroupService.onDidChangeActiveGroup(this._onDidChangeActiveGroup.fire, this._onDidChangeActiveGroup)); + this._register(this._terminalInstanceService.onDidCreateInstance(instance => { this._initInstanceListeners(instance); this._onDidCreateInstance.fire(instance); - }); + })); // Hide the panel if there are no more instances, provided that VS Code is not shutting // down. When shutting down the panel is locked in place so that it is restored upon next // launch. - this._terminalGroupService.onDidChangeActiveInstance(instance => { + this._register(this._terminalGroupService.onDidChangeActiveInstance(instance => { if (!instance && !this._isShuttingDown) { this._terminalGroupService.hidePanel(); } @@ -211,7 +211,7 @@ export class TerminalService extends Disposable implements ITerminalService { } else if (!instance) { this._terminalShellTypeContextKey.reset(); } - }); + })); this._handleInstanceContextKeys(); this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService); @@ -228,7 +228,7 @@ export class TerminalService extends Disposable implements ITerminalService { _lifecycleService.onBeforeShutdown(async e => e.veto(this._onBeforeShutdown(e.reason), 'veto.terminal')); _lifecycleService.onWillShutdown(e => this._onWillShutdown(e)); - this.initializePrimaryBackend(); + this._initializePrimaryBackend(); // Create async as the class depends on `this` timeout(0).then(() => this._register(this._instantiationService.createInstance(TerminalEditorStyle, mainWindow.document.head))); @@ -273,7 +273,7 @@ export class TerminalService extends Disposable implements ITerminalService { return undefined; } - async initializePrimaryBackend() { + private async _initializePrimaryBackend() { mark('code/terminal/willGetTerminalBackend'); this._primaryBackend = await this._terminalInstanceService.getBackend(this._environmentService.remoteAuthority); mark('code/terminal/didGetTerminalBackend'); @@ -520,14 +520,14 @@ export class TerminalService extends Disposable implements ITerminalService { } private _attachProcessLayoutListeners(): void { - this.onDidChangeActiveGroup(() => this._saveState()); - this.onDidChangeActiveInstance(() => this._saveState()); - this.onDidChangeInstances(() => this._saveState()); + this._register(this.onDidChangeActiveGroup(() => this._saveState())); + this._register(this.onDidChangeActiveInstance(() => this._saveState())); + this._register(this.onDidChangeInstances(() => this._saveState())); // The state must be updated when the terminal is relaunched, otherwise the persistent // terminal ID will be stale and the process will be leaked. - this.onAnyInstanceProcessIdReady(() => this._saveState()); - this.onAnyInstanceTitleChange(instance => this._updateTitle(instance)); - this.onAnyInstanceIconChange(e => this._updateIcon(e.instance, e.userInitiated)); + this._register(this.onAnyInstanceProcessIdReady(() => this._saveState())); + this._register(this.onAnyInstanceTitleChange(instance => this._updateTitle(instance))); + this._register(this.onAnyInstanceIconChange(e => this._updateIcon(e.instance, e.userInitiated))); } private _handleInstanceContextKeys(): void { @@ -1253,7 +1253,7 @@ class TerminalEditorStyle extends Themable { if (uri instanceof URI && iconClasses && iconClasses.length > 1) { css += ( `.monaco-workbench .terminal-tab.${iconClasses[0]}::before` + - `{background-image: ${dom.asCSSUrl(uri)};}` + `{content: ''; background-image: ${dom.asCSSUrl(uri)};}` ); } if (ThemeIcon.isThemeIcon(icon)) { diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index ebd4abf5e19..638ad6e95c1 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -54,7 +54,7 @@ export function getShellIntegrationTooltip(instance: ITerminalInstance, markdown export function getShellProcessTooltip(instance: ITerminalInstance, markdown: boolean): string { const lines: string[] = []; - if (instance.processId) { + if (instance.processId && instance.processId > 0) { lines.push(localize({ key: 'shellProcessTooltip.processId', comment: ['The first arg is "PID" which shouldn\'t be translated'] }, "Process ID ({0}): {1}", 'PID', instance.processId) + '\n'); } diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 31ed968488e..7bf4f427bd0 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -44,7 +44,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Event } from 'vs/base/common/event'; -import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { InstanceContext, TerminalContextActionRunner } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts index 9ff52e29fc5..795b1a7de3f 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts @@ -77,7 +77,7 @@ export class TerminalVoiceSession extends Disposable { this._disposables = this._register(new DisposableStore()); } - start(): void { + async start(): Promise { this.stop(); let voiceTimeout = this.configurationService.getValue(AccessibilityVoiceSettingId.SpeechTimeout); if (!isNumber(voiceTimeout) || voiceTimeout < 0) { @@ -89,7 +89,7 @@ export class TerminalVoiceSession extends Disposable { }, voiceTimeout)); this._cancellationTokenSource = new CancellationTokenSource(); this._register(toDisposable(() => this._cancellationTokenSource?.dispose(true))); - const session = this._speechService.createSpeechToTextSession(this._cancellationTokenSource?.token); + const session = await this._speechService.createSpeechToTextSession(this._cancellationTokenSource?.token); this._disposables.add(session.onDidChange((e) => { if (this._cancellationTokenSource?.token.isCancellationRequested) { diff --git a/code/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts b/code/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts index 499e1d2f57d..9a2fbf8e6d7 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts @@ -7,12 +7,14 @@ import { coalesce } from 'vs/base/common/arrays'; import { Disposable, DisposableStore, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; import { IMarkTracker } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import type { Terminal, IMarker, ITerminalAddon, IDecoration } from '@xterm/xterm'; +import type { Terminal, IMarker, ITerminalAddon, IDecoration, IBufferRange } from '@xterm/xterm'; import { timeout } from 'vs/base/common/async'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { getWindow } from 'vs/base/browser/dom'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; enum Boundary { Top, @@ -24,6 +26,13 @@ export const enum ScrollPosition { Middle } +interface IScrollToMarkerOptions { + hideDecoration?: boolean; + /** Scroll even if the line is within the viewport */ + forceScroll?: boolean; + bufferRange?: IBufferRange; +} + export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITerminalAddon { private _currentMarker: IMarker | Boundary = Boundary.Bottom; private _selectionStart: IMarker | Boundary | null = null; @@ -43,6 +52,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe constructor( private readonly _capabilities: ITerminalCapabilityStore, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IThemeService private readonly _themeService: IThemeService ) { super(); @@ -91,7 +101,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe return undefined; } - clearMarker(): void { + clear(): void { // Clear the current marker so successive focus/selection actions are performed from the // bottom of the buffer this._currentMarker = Boundary.Bottom; @@ -219,16 +229,20 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe } } - private _scrollToMarker(start: IMarker | number, position: ScrollPosition, end?: IMarker | number, hideDecoration?: boolean): void { + private _scrollToMarker(start: IMarker | number, position: ScrollPosition, end?: IMarker | number, options?: IScrollToMarkerOptions): void { if (!this._terminal) { return; } - if (!this._isMarkerInViewport(this._terminal, start)) { + if (!this._isMarkerInViewport(this._terminal, start) || options?.forceScroll) { const line = this.getTargetScrollLine(toLineIndex(start), position); this._terminal.scrollToLine(line); } - if (!hideDecoration) { - this.registerTemporaryDecoration(start, end, true); + if (!options?.hideDecoration) { + if (options?.bufferRange) { + this._highlightBufferRange(options.bufferRange); + } else { + this.registerTemporaryDecoration(start, end, true); + } } } @@ -260,6 +274,19 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe ); } + revealRange(range: IBufferRange): void { + this._scrollToMarker( + range.start.y - 1, + ScrollPosition.Middle, + range.end.y - 1, + { + bufferRange: range, + // Ensure scroll shows the line when sticky scroll is enabled + forceScroll: !!this._configurationService.getValue(TerminalSettingId.StickyScrollEnabled) + } + ); + } + showCommandGuide(command: ITerminalCommand | undefined): void { if (!this._terminal) { return; @@ -314,6 +341,50 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe } } + + private _scrollState: { viewportY: number } | undefined; + + saveScrollState(): void { + this._scrollState = { viewportY: this._terminal?.buffer.active.viewportY ?? 0 }; + } + + restoreScrollState(): void { + if (this._scrollState && this._terminal) { + this._terminal.scrollToLine(this._scrollState.viewportY); + this._scrollState = undefined; + } + } + + private _highlightBufferRange(range: IBufferRange): void { + if (!this._terminal) { + return; + } + + this._resetNavigationDecorations(); + const startLine = range.start.y; + const decorationCount = range.end.y - range.start.y + 1; + for (let i = 0; i < decorationCount; i++) { + const decoration = this._terminal.registerDecoration({ + marker: this._createMarkerForOffset(startLine - 1, i), + x: range.start.x - 1, + width: (range.end.x - 1) - (range.start.x - 1) + 1, + overviewRulerOptions: undefined + }); + if (decoration) { + this._navigationDecorations?.push(decoration); + let renderedElement: HTMLElement | undefined; + + decoration.onRender(element => { + if (!renderedElement) { + renderedElement = element; + element.classList.add('terminal-range-highlight'); + } + }); + decoration.onDispose(() => { this._navigationDecorations = this._navigationDecorations?.filter(d => d !== decoration); }); + } + } + } + registerTemporaryDecoration(marker: IMarker | number, endMarker: IMarker | number | undefined, showOutline: boolean): void { if (!this._terminal) { return; @@ -373,7 +444,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe } getTargetScrollLine(line: number, position: ScrollPosition): number { - // Middle is treated at 1/4 of the viewport's size because context below is almost always + // Middle is treated as 1/4 of the viewport's size because context below is almost always // more important than context above in the terminal. if (this._terminal && position === ScrollPosition.Middle) { return Math.max(line - Math.floor(this._terminal.rows / 4), 0); @@ -397,7 +468,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe return; } const endMarker = endMarkerId ? detectionCapability.getMark(endMarkerId) : startMarker; - this._scrollToMarker(startMarker, ScrollPosition.Top, endMarker, !highlight); + this._scrollToMarker(startMarker, ScrollPosition.Top, endMarker, { hideDecoration: !highlight }); } selectToPreviousMark(): void { diff --git a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 0f2ef8f01f4..c304610faa7 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -244,7 +244,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach scrollSensitivity: config.mouseWheelScrollSensitivity, wordSeparator: config.wordSeparators, overviewRulerWidth: 10, - ignoreBracketedPasteMode: config.ignoreBracketedPasteMode + ignoreBracketedPasteMode: config.ignoreBracketedPasteMode, + rescaleOverlappingGlyphs: config.rescaleOverlappingGlyphs, })); this._updateSmoothScrolling(); this._core = (this.raw as any)._core as IXtermCore; @@ -404,6 +405,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.options.wordSeparator = config.wordSeparators; this.raw.options.customGlyphs = config.customGlyphs; this.raw.options.ignoreBracketedPasteMode = config.ignoreBracketedPasteMode; + this.raw.options.rescaleOverlappingGlyphs = config.rescaleOverlappingGlyphs; this._updateSmoothScrolling(); if (this._attached?.options.enableGpu) { if (this._shouldLoadWebgl()) { diff --git a/code/src/vs/workbench/contrib/terminal/common/terminal.ts b/code/src/vs/workbench/contrib/terminal/common/terminal.ts index fc925b646bb..d2ac113bfbd 100644 --- a/code/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/code/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -204,6 +204,7 @@ export interface ITerminalConfiguration { enableImages: boolean; smoothScrolling: boolean; ignoreBracketedPasteMode: boolean; + rescaleOverlappingGlyphs: boolean; } export const DEFAULT_LOCAL_ECHO_EXCLUDE: ReadonlyArray = ['vim', 'vi', 'nano', 'tmux']; @@ -649,7 +650,18 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ 'workbench.action.quickOpenView', 'workbench.action.toggleMaximizedPanel', 'notification.acceptPrimaryAction', - 'runCommands' + 'runCommands', + 'workbench.action.terminal.chat.start', + 'workbench.action.terminal.chat.close', + 'workbench.action.terminal.chat.discard', + 'workbench.action.terminal.chat.makeRequest', + 'workbench.action.terminal.chat.cancel', + 'workbench.action.terminal.chat.feedbackHelpful', + 'workbench.action.terminal.chat.feedbackUnhelpful', + 'workbench.action.terminal.chat.feedbackReportIssue', + 'workbench.action.terminal.chat.runCommand', + 'workbench.action.terminal.chat.insertCommand', + 'workbench.action.terminal.chat.viewInChat', ]; export const terminalContributionsDescriptor: IExtensionPointDescriptor = { diff --git a/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 91293c6594d..d92016a9dc6 100644 --- a/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/code/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -560,6 +560,11 @@ const terminalConfiguration: IConfigurationNode = { type: 'boolean', default: true }, + [TerminalSettingId.RescaleOverlappingGlyphs]: { + markdownDescription: localize('terminal.integrated.rescaleOverlappingGlyphs', "Whether to rescale glyphs horizontally that are a single cell wide but have glyphs that would overlap following cell(s). This typically happens for ambiguous width characters (eg. the roman numeral characters U+2160+) which aren't featured in monospace fonts. Emoji glyphs are never rescaled."), + type: 'boolean', + default: true + }, [TerminalSettingId.AutoReplies]: { markdownDescription: localize('terminal.integrated.autoReplies', "A set of messages that, when encountered in the terminal, will be automatically responded to. Provided the message is specific enough, this can help automate away common responses.\n\nRemarks:\n\n- Use {0} to automatically respond to the terminate batch job prompt on Windows.\n- The message includes escape sequences so the reply might not happen with styled text.\n- Each reply can only happen once every second.\n- Use {1} in the reply to mean the enter key.\n- To unset a default key, set the value to null.\n- Restart VS Code if new don't apply.", '`"Terminate batch job (Y/N)": "Y\\r"`', '`"\\r"`'), type: 'object', @@ -601,7 +606,8 @@ const terminalConfiguration: IConfigurationNode = { restricted: true, markdownDescription: localize('terminal.integrated.shellIntegration.suggestEnabled', "Enables experimental terminal Intellisense suggestions for supported shells when {0} is set to {1}. If shell integration is installed manually, {2} needs to be set to {3} before calling the script.", '`#terminal.integrated.shellIntegration.enabled#`', '`true`', '`VSCODE_SUGGEST`', '`1`'), type: 'boolean', - default: false + default: false, + markdownDeprecationMessage: localize('suggestEnabled.deprecated', 'This is an experimental setting and may break the terminal! Use at your own risk.') }, [TerminalSettingId.SmoothScrolling]: { markdownDescription: localize('terminal.integrated.smoothScrolling', "Controls whether the terminal will scroll using an animation."), @@ -659,6 +665,11 @@ const terminalConfiguration: IConfigurationNode = { type: 'boolean', default: false }, + [TerminalSettingId.ExperimentalInlineChat]: { + markdownDescription: localize('terminal.integrated.experimentalInlineChat', "Whether to enable the upcoming experimental inline terminal chat UI."), + type: 'boolean', + default: false + } } }; diff --git a/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 06779c99e21..7f40100cf84 100644 --- a/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -37,6 +37,7 @@ import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statu import { memoize } from 'vs/base/common/decorators'; import { StopWatch } from 'vs/base/common/stopwatch'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; export class LocalTerminalBackendContribution implements IWorkbenchContribution { @@ -94,11 +95,11 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke ) { super(_localPtyService, logService, historyService, _configurationResolverService, statusBarService, workspaceContextService); - this.onPtyHostRestart(() => { + this._register(this.onPtyHostRestart(() => { this._directProxy = undefined; this._directProxyClientEventually = undefined; this._connectToDirectProxy(); - }); + })); } /** @@ -373,7 +374,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); - if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const workspaceFolder = terminalEnvironment.getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver); } diff --git a/code/src/vs/workbench/contrib/terminal/terminal.all.ts b/code/src/vs/workbench/contrib/terminal/terminal.all.ts index b49aa829f7b..e9cdc253212 100644 --- a/code/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/code/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -17,6 +17,7 @@ import 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.acce import 'vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution'; import 'vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution'; import 'vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution'; +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution'; import 'vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution'; import 'vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution'; import 'vs/workbench/contrib/terminalContrib/zoom/browser/terminal.zoom.contribution'; diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css new file mode 100644 index 00000000000..e515158cd77 --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.terminal-inline-chat { + position: absolute; + left: 0; + bottom: 0; + z-index: 100; + height: auto !important; +} + +.terminal-inline-chat .inline-chat { + margin-top: 0 !important; +} + +.terminal-inline-chat.hide { + visibility: hidden; +} + +.terminal-inline-chat .chatMessageContent .value { + padding-top: 10px; +} + +.terminal-inline-chat .inline-chat-input .monaco-editor-background { + /* Override the global panel rule for monaco backgrounds */ + background-color: var(--vscode-inlineChatInput-background) !important; +} diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts new file mode 100644 index 00000000000..44eabbf13f6 --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; +import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; +import { TerminalChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; + +registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); + +registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(TerminalChatAccessibilityHelpContribution.ID, TerminalChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts new file mode 100644 index 00000000000..6a8b0b8854c --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const enum TerminalChatCommandId { + Start = 'workbench.action.terminal.chat.start', + Close = 'workbench.action.terminal.chat.close', + FocusResponse = 'workbench.action.terminal.chat.focusResponse', + FocusInput = 'workbench.action.terminal.chat.focusInput', + Discard = 'workbench.action.terminal.chat.discard', + MakeRequest = 'workbench.action.terminal.chat.makeRequest', + Cancel = 'workbench.action.terminal.chat.cancel', + FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', + FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', + FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', + RunCommand = 'workbench.action.terminal.chat.runCommand', + InsertCommand = 'workbench.action.terminal.chat.insertCommand', + ViewInChat = 'workbench.action.terminal.chat.viewInChat', + PreviousFromHistory = 'workbench.action.terminal.chat.previousFromHistory', + NextFromHistory = 'workbench.action.terminal.chat.nextFromHistory', +} + +export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); +export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget'); +export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status'); +export const MENU_TERMINAL_CHAT_WIDGET_FEEDBACK = MenuId.for('terminalChatWidget.feedback'); +export const MENU_TERMINAL_CHAT_WIDGET_TOOLBAR = MenuId.for('terminalChatWidget.toolbar'); + +export const enum TerminalChatContextKeyStrings { + ChatFocus = 'terminalChatFocus', + ChatVisible = 'terminalChatVisible', + ChatActiveRequest = 'terminalChatActiveRequest', + ChatInputHasText = 'terminalChatInputHasText', + ChatAgentRegistered = 'terminalChatAgentRegistered', + ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', + ChatResponseContainsCodeBlock = 'terminalChatResponseContainsCodeBlock', + ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting', + ChatSessionResponseVote = 'terminalChatSessionResponseVote', +} + + +export namespace TerminalChatContextKeys { + + /** Whether the chat widget is focused */ + export const focused = new RawContextKey(TerminalChatContextKeyStrings.ChatFocus, false, localize('chatFocusedContextKey', "Whether the chat view is focused.")); + + /** Whether the chat widget is visible */ + export const visible = new RawContextKey(TerminalChatContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); + + /** Whether there is an active chat request */ + export const requestActive = new RawContextKey(TerminalChatContextKeyStrings.ChatActiveRequest, false, localize('chatRequestActiveContextKey', "Whether there is an active chat request.")); + + /** Whether the chat input has text */ + export const inputHasText = new RawContextKey(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); + + /** Whether the terminal chat agent has been registered */ + export const agentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); + + /** The type of chat response, if any */ + export const responseContainsCodeBlock = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseContainsCodeBlock, false, localize('chatResponseContainsCodeBlockContextKey', "Whether the chat response contains a code block.")); + + /** Whether the response supports issue reporting */ + export const responseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); + + /** The chat vote, if any for the response, if any */ + export const sessionResponseVote = new RawContextKey(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); +} diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts new file mode 100644 index 00000000000..584bdc753d8 --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.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 { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +export class TerminalChatAccessibilityHelpContribution extends Disposable { + static ID = 'terminalChatAccessiblityHelp'; + constructor() { + super(); + this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.focused)); + } +} + +export async function runAccessibilityHelpAction(accessor: ServicesAccessor): Promise { + const accessibleViewService = accessor.get(IAccessibleViewService); + const terminalService = accessor.get(ITerminalService); + + const instance = terminalService.activeInstance; + if (!instance) { + return; + } + + const helpText = getAccessibilityHelpText(accessor); + accessibleViewService.show({ + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.TerminalChat, + provideContent: () => helpText, + onClose: () => TerminalChatController.get(instance)?.focus(), + options: { type: AccessibleViewType.Help } + }); +} + +export function getAccessibilityHelpText(accessor: ServicesAccessor): string { + const keybindingService = accessor.get(IKeybindingService); + const content = []; + const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); + const runCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.RunCommand)?.getAriaLabel(); + const insertCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.InsertCommand)?.getAriaLabel(); + const makeRequestKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.MakeRequest)?.getAriaLabel(); + const startChatKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.Start)?.getAriaLabel(); + const focusResponseKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.FocusResponse)?.getAriaLabel(); + const focusInputKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.FocusInput)?.getAriaLabel(); + content.push(localize('inlineChat.overview', "Inline chat occurs within a terminal. It is useful for suggesting terminal commands. Keep in mind that AI generated code may be incorrect.")); + content.push(localize('inlineChat.access', "It can be activated using the command: Terminal: Start Chat ({0}), which will focus the input box.", startChatKeybinding)); + content.push(makeRequestKeybinding ? localize('inlineChat.input', "The input box is where the user can type a request and can make the request ({0}). The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.", makeRequestKeybinding) : localize('inlineChat.inputNoKb', "The input box is where the user can type a request and can make the request by tabbing to the Make Request button, which is not currently triggerable via keybindings. The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.")); + content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponseMessage', 'The response can be inspected in the accessible view ({0}).', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(focusResponseKeybinding ? localize('inlineChat.focusResponse', 'Reach the response from the input box ({0}).', focusResponseKeybinding) : localize('inlineChat.focusResponseNoKb', 'Reach the response from the input box by tabbing or assigning a keybinding for the command: Focus Terminal Response.')); + content.push(focusInputKeybinding ? localize('inlineChat.focusInput', 'Reach the input box from the response ({0}).', focusInputKeybinding) : localize('inlineChat.focusInputNoKb', 'Reach the response from the input box by shift+tabbing or assigning a keybinding for the command: Focus Terminal Input.')); + content.push(runCommandKeybinding ? localize('inlineChat.runCommand', 'With focus in the input box or command editor, the Terminal: Run Chat Command ({0}) action.', runCommandKeybinding) : localize('inlineChat.runCommandNoKb', 'Run a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); + content.push(insertCommandKeybinding ? localize('inlineChat.insertCommand', 'With focus in the input box command editor, the Terminal: Insert Chat Command ({0}) action.', insertCommandKeybinding) : localize('inlineChat.insertCommandNoKb', 'Insert a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); + content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); + content.push(localize('chat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring.")); + return content.join('\n\n'); +} diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts new file mode 100644 index 00000000000..f5bbe82de03 --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +export class TerminalInlineChatAccessibleViewContribution extends Disposable { + static ID: 'terminalInlineChatAccessibleViewContribution'; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(105, 'terminalInlineChat', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const terminalService = accessor.get(ITerminalService); + const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; + if (!controller?.lastResponseContent) { + return false; + } + const responseContent = controller.lastResponseContent; + accessibleViewService.show({ + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.View } + }); + return true; + }, TerminalChatContextKeys.focused)); + } +} diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts new file mode 100644 index 00000000000..652aa927071 --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -0,0 +1,408 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { localize2 } from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { AbstractInlineChatAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +registerActiveXtermAction({ + id: TerminalChatCommandId.Start, + title: localize2('startChat', 'Start in Terminal'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyI, + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny), + // HACK: Force weight to be higher than the extension contributed keybinding to override it until it gets replaced + weight: KeybindingWeight.ExternalExtension + 1, // KeybindingWeight.WorkbenchContrib, + }, + f1: true, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + // TODO: This needs to change to check for a terminal location capable agent + CTX_INLINE_CHAT_HAS_PROVIDER + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.reveal(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.Close, + title: localize2('closeChat', 'Close Chat'), + keybinding: { + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.visible), + weight: KeybindingWeight.WorkbenchContrib, + }, + icon: Codicon.close, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 2 + }, + f1: true, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.visible) + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.clear(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FocusResponse, + title: localize2('focusTerminalResponse', 'Focus Terminal Response'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + }, + f1: true, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.focused + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.focusResponse(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FocusInput, + title: localize2('focusTerminalInput', 'Focus Terminal Input'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + }, + f1: true, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.focused + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.focus(); + } +}); + + +registerActiveXtermAction({ + id: TerminalChatCommandId.Discard, + title: localize2('discard', 'Discard'), + icon: Codicon.discard, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 2, + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.responseContainsCodeBlock) + }, + f1: true, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.focused, + TerminalChatContextKeys.responseContainsCodeBlock + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.clear(); + } +}); + + +registerActiveXtermAction({ + id: TerminalChatCommandId.RunCommand, + title: localize2('runCommand', 'Run Chat Command'), + shortTitle: localize2('run', 'Run'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + TerminalChatContextKeys.responseContainsCodeBlock + ), + icon: Codicon.play, + keybinding: { + when: TerminalChatContextKeys.requestActive.negate(), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + }, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 0, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()) + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptCommand(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.InsertCommand, + title: localize2('insertCommand', 'Insert Chat Command'), + shortTitle: localize2('insert', 'Insert'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + TerminalChatContextKeys.responseContainsCodeBlock + ), + keybinding: { + when: TerminalChatContextKeys.requestActive.negate(), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Alt | KeyCode.Enter, + }, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()) + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptCommand(false); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.ViewInChat, + title: localize2('viewInChat', 'View in Chat'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + ), + icon: Codicon.commentDiscussion, + menu: [{ + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.negate(), TerminalChatContextKeys.requestActive.negate()), + }, + { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), + }], + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.viewInChat(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.MakeRequest, + title: localize2('makeChatRequest', 'Make Chat Request'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + CTX_INLINE_CHAT_EMPTY.negate() + ), + icon: Codicon.send, + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalChatContextKeys.requestActive.negate()), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Enter + }, + menu: { + id: MENU_TERMINAL_CHAT_INPUT, + group: 'main', + order: 1, + when: TerminalChatContextKeys.requestActive.negate(), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptInput(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.Cancel, + title: localize2('cancelChat', 'Cancel Chat'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.requestActive, + TerminalChatContextKeys.agentRegistered + ), + icon: Codicon.debugStop, + menu: { + id: MENU_TERMINAL_CHAT_INPUT, + group: 'main', + when: TerminalChatContextKeys.requestActive, + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.cancel(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackHelpful, + title: localize2('feedbackHelpful', 'Helpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined) + ), + icon: Codicon.thumbsup, + toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('up'), + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 1, + when: TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackUnhelpful, + title: localize2('feedbackUnhelpful', 'Unhelpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), + ), + toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('down'), + icon: Codicon.thumbsdown, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 2, + when: TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(false); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackReportIssue, + title: localize2('reportIssue', 'Report Issue'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), + TerminalChatContextKeys.responseSupportsIssueReporting + ), + icon: Codicon.report, + menu: [{ + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting), + group: 'inline', + order: 3 + }], + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.PreviousFromHistory, + title: localize2('previousFromHistory', 'Previous From History'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined) + ), + keybinding: { + weight: KeybindingWeight.EditorCore + 10, // win against core_command + primary: KeyCode.UpArrow, + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.populateHistory(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.NextFromHistory, + title: localize2('nextFromHistory', 'Next From History'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined) + ), + keybinding: { + weight: KeybindingWeight.EditorCore + 10, // win against core_command + primary: KeyCode.DownArrow, + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.populateHistory(false); + } +}); diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts new file mode 100644 index 00000000000..229efccc7a1 --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -0,0 +1,405 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAgentRequest, IChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatUserAction, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { ChatModel, ChatRequestModel, IChatRequestVariableData, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { InlineChatHistory } from 'vs/workbench/contrib/inlineChat/browser/inlineChatHistory'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; + +const enum Message { + NONE = 0, + ACCEPT_SESSION = 1 << 0, + CANCEL_SESSION = 1 << 1, + PAUSE_SESSION = 1 << 2, + CANCEL_REQUEST = 1 << 3, + CANCEL_INPUT = 1 << 4, + ACCEPT_INPUT = 1 << 5, + RERUN_INPUT = 1 << 6, +} + +export class TerminalChatController extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.chat'; + + static get(instance: ITerminalInstance): TerminalChatController | null { + return instance.getContribution(TerminalChatController.ID); + } + /** + * Currently focused chat widget. This is used to track action context since 'active terminals' + * are only tracked for non-detached terminal instanecs. + */ + static activeChatWidget?: TerminalChatController; + + /** + * The chat widget for the controller, this is lazy as we don't want to instantiate it until + * both it's required and xterm is ready. + */ + private _chatWidget: Lazy | undefined; + + /** + * The chat widget for the controller, this will be undefined if xterm is not ready yet (ie. the + * terminal is still initializing). + */ + get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } + + private readonly _requestActiveContextKey: IContextKey; + private readonly _terminalAgentRegisteredContextKey: IContextKey; + private readonly _responseContainsCodeBlockContextKey: IContextKey; + private readonly _responseSupportsIssueReportingContextKey: IContextKey; + private readonly _sessionResponseVoteContextKey: IContextKey; + + private _messages = this._store.add(new Emitter()); + + private _currentRequest: ChatRequestModel | undefined; + + private readonly _history: InlineChatHistory; + private _lastInput: string | undefined; + private _lastResponseContent: string | undefined; + get lastResponseContent(): string | undefined { + return this._lastResponseContent; + } + + readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); + readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); + + private _terminalAgentId = 'terminal'; + + private _model: MutableDisposable = this._register(new MutableDisposable()); + + constructor( + private readonly _instance: ITerminalInstance, + processManager: ITerminalProcessManager, + widgetManager: TerminalWidgetManager, + @IConfigurationService private _configurationService: IConfigurationService, + @ITerminalService private readonly _terminalService: ITerminalService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IChatService private readonly _chatService: IChatService, + @IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService + ) { + super(); + + this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService); + this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService); + this._responseContainsCodeBlockContextKey = TerminalChatContextKeys.responseContainsCodeBlock.bindTo(this._contextKeyService); + this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); + this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); + + this._history = this._instantiationService.createInstance(InlineChatHistory, 'terminal-chat-history'); + + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + + if (!this._chatAgentService.getAgent(this._terminalAgentId)) { + this._register(this._chatAgentService.onDidChangeAgents(() => { + if (this._chatAgentService.getAgent(this._terminalAgentId)) { + this._terminalAgentRegisteredContextKey.set(true); + } + })); + } else { + this._terminalAgentRegisteredContextKey.set(true); + } + this._register(this._chatCodeBlockContextProviderService.registerProvider({ + getCodeBlockContext: (editor) => { + const chatWidget = this.chatWidget; + if (!chatWidget) { + return; + } + if (!editor) { + return; + } + return { + element: editor, + code: editor.getValue(), + codeBlockIndex: 0, + languageId: editor.getModel()!.getLanguageId() + }; + } + }, 'terminal')); + } + + xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + this._chatWidget = new Lazy(() => { + const chatWidget = this._register(this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance)); + this._register(chatWidget.focusTracker.onDidFocus(() => { + TerminalChatController.activeChatWidget = this; + if (!isDetachedTerminalInstance(this._instance)) { + this._terminalService.setActiveInstance(this._instance); + } + })); + this._register(chatWidget.focusTracker.onDidBlur(() => { + TerminalChatController.activeChatWidget = undefined; + this._instance.resetScrollbarVisibility(); + })); + if (!this._instance.domElement) { + throw new Error('FindWidget expected terminal DOM to be initialized'); + } + return chatWidget; + }); + } + + acceptFeedback(helpful?: boolean): void { + const providerId = this._chatService.getProviderInfos()?.[0]?.id; + const model = this._model.value; + if (!providerId || !this._currentRequest || !model) { + return; + } + let action: ChatUserAction; + if (helpful === undefined) { + action = { kind: 'bug' }; + } else { + this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down'); + action = { kind: 'vote', direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down }; + } + // TODO:extract into helper method + for (const request of model.getRequests()) { + if (request.response?.response.value || request.response?.result) { + this._chatService.notifyUserAction({ + providerId, + sessionId: request.session.sessionId, + requestId: request.id, + agentId: request.response?.agent?.id, + result: request.response?.result, + action + }); + } + } + this._chatWidget?.value.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); + } + + cancel(): void { + if (this._currentRequest) { + this._model.value?.cancelRequest(this._currentRequest); + } + this._requestActiveContextKey.set(false); + this._chatWidget?.value.inlineChatWidget.updateProgress(false); + this._chatWidget?.value.inlineChatWidget.updateInfo(''); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); + } + + private _forcedPlaceholder: string | undefined = undefined; + + private _updatePlaceholder(): void { + const inlineChatWidget = this._chatWidget?.value.inlineChatWidget; + if (inlineChatWidget) { + inlineChatWidget.placeholder = this._getPlaceholderText(); + } + } + + private _getPlaceholderText(): string { + return this._forcedPlaceholder ?? ''; + } + + setPlaceholder(text: string): void { + this._forcedPlaceholder = text; + this._updatePlaceholder(); + } + + resetPlaceholder(): void { + this._forcedPlaceholder = undefined; + this._updatePlaceholder(); + } + + clear(): void { + if (this._currentRequest) { + this._model.value?.cancelRequest(this._currentRequest); + } + this._model.clear(); + this._chatWidget?.value.hide(); + this._chatWidget?.value.setValue(undefined); + this._responseContainsCodeBlockContextKey.reset(); + this._sessionResponseVoteContextKey.reset(); + this._requestActiveContextKey.reset(); + } + + async acceptInput(): Promise { + const providerInfo = this._chatService.getProviderInfos()?.[0]; + if (!providerInfo) { + return; + } + if (!this._model.value) { + this._model.value = this._chatService.startSession(providerInfo.id, CancellationToken.None); + if (!this._model.value) { + throw new Error('Could not start chat session'); + } + } + const model = this._model.value; + + this._lastInput = this._chatWidget?.value?.input(); + if (!this._lastInput) { + return; + } + const accessibilityRequestId = this._chatAccessibilityService.acceptRequest(); + this._requestActiveContextKey.set(true); + const cancellationToken = new CancellationTokenSource().token; + let responseContent = ''; + const progressCallback = (progress: IChatProgress) => { + if (cancellationToken.isCancellationRequested) { + return; + } + + if (progress.kind === 'content' || progress.kind === 'markdownContent') { + responseContent += progress.content; + } + if (this._currentRequest) { + model.acceptResponseProgress(this._currentRequest, progress); + } + }; + + await model.waitForInitialization(); + this._history.update(this._lastInput); + const request: IParsedChatRequest = { + text: this._lastInput, + parts: [] + }; + const requestVarData: IChatRequestVariableData = { + variables: [] + }; + this._currentRequest = model.addRequest(request, requestVarData); + const requestProps: IChatAgentRequest = { + sessionId: model.sessionId, + requestId: this._currentRequest!.id, + agentId: this._terminalAgentId, + message: this._lastInput, + variables: { variables: [] }, + location: ChatAgentLocation.Terminal + }; + try { + const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken); + this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined); + this._chatWidget?.value.inlineChatWidget.updateFollowUps(undefined); + this._chatWidget?.value.inlineChatWidget.updateProgress(true); + this._chatWidget?.value.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026'); + await task; + } catch (e) { + + } finally { + this._requestActiveContextKey.set(false); + this._chatWidget?.value.inlineChatWidget.updateProgress(false); + this._chatWidget?.value.inlineChatWidget.updateInfo(''); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); + if (this._currentRequest) { + model.completeResponse(this._currentRequest); + } + this._lastResponseContent = responseContent; + if (this._currentRequest) { + this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); + const containsCode = responseContent.includes('```'); + this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id, providerId: 'terminal' }, false, containsCode); + this._responseContainsCodeBlockContextKey.set(containsCode); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); + this._messages.fire(Message.ACCEPT_INPUT); + } + const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; + if (supportIssueReporting !== undefined) { + this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); + } + } + } + + updateInput(text: string, selectAll = true): void { + const widget = this._chatWidget?.value.inlineChatWidget; + if (widget) { + widget.value = text; + if (selectAll) { + widget.selectAll(); + } + } + } + + getInput(): string { + return this._chatWidget?.value.input() ?? ''; + } + + focus(): void { + this._chatWidget?.value.focus(); + } + + hasFocus(): boolean { + return !!this._chatWidget?.value.hasFocus(); + } + + async acceptCommand(shouldExecute: boolean): Promise { + const code = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); + if (!code) { + return; + } + this._chatWidget?.value.acceptCommand(code.object.textEditorModel.getValue(), shouldExecute); + } + + reveal(): void { + this._chatWidget?.value.reveal(); + this._history.clearCandidate(); + } + + async viewInChat(): Promise { + const providerInfo = this._chatService.getProviderInfos()?.[0]; + if (!providerInfo) { + return; + } + const model = this._model.value; + const widget = await this._chatWidgetService.revealViewForProvider(providerInfo.id); + if (widget) { + if (widget.viewModel && model) { + for (const request of model.getRequests()) { + if (request.response?.response.value || request.response?.result) { + this._chatService.addCompleteRequest(widget.viewModel.sessionId, + request.message as IParsedChatRequest, + request.variableData, + { + message: request.response.response.value, + result: request.response.result, + followups: request.response.followups + }); + } + } + widget.focusLastMessage(); + } else if (!model) { + widget.focusInput(); + } + this._chatWidget?.value.hide(); + } + } + + populateHistory(up: boolean) { + const entry = this._history.populateHistory(this.getInput(), up); + if (entry) { + this.updateInput(entry, true); + } + } + + // TODO: Move to register calls, don't override + override dispose() { + if (this._currentRequest) { + this._model.value?.cancelRequest(this._currentRequest); + } + super.dispose(); + this.clear(); + } +} diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts new file mode 100644 index 00000000000..3be5fe74c05 --- /dev/null +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./media/terminalChatWidget'; +import { localize } from 'vs/nls'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; + +const enum Constants { + HorizontalMargin = 10 +} + +export class TerminalChatWidget extends Disposable { + + private readonly _container: HTMLElement; + + private readonly _inlineChatWidget: InlineChatWidget; + public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } + + private readonly _focusTracker: IFocusTracker; + + private readonly _focusedContextKey: IContextKey; + private readonly _visibleContextKey: IContextKey; + + constructor( + private readonly _terminalElement: HTMLElement, + private readonly _instance: ITerminalInstance, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(); + + this._focusedContextKey = TerminalChatContextKeys.focused.bindTo(this._contextKeyService); + this._visibleContextKey = TerminalChatContextKeys.visible.bindTo(this._contextKeyService); + + this._container = document.createElement('div'); + this._container.classList.add('terminal-inline-chat'); + _terminalElement.appendChild(this._container); + + this._inlineChatWidget = this._instantiationService.createInstance( + InlineChatWidget, + { + inputMenuId: MENU_TERMINAL_CHAT_INPUT, + widgetMenuId: MENU_TERMINAL_CHAT_WIDGET, + statusMenuId: { + menu: MENU_TERMINAL_CHAT_WIDGET_STATUS, + options: { + buttonConfigProvider: action => { + if (action.id === TerminalChatCommandId.ViewInChat || action.id === TerminalChatCommandId.RunCommand) { + return { isSecondary: false }; + } else { + return { isSecondary: true }; + } + } + } + }, + feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + telemetrySource: 'terminal-inline-chat' + } + ); + this._register(Event.any( + this._inlineChatWidget.onDidChangeHeight, + this._instance.onDimensionsChanged, + )(() => this._relayout())); + + const observer = new ResizeObserver(() => this._relayout()); + observer.observe(this._terminalElement); + this._register(toDisposable(() => observer.disconnect())); + + this._reset(); + this._container.appendChild(this._inlineChatWidget.domNode); + + this._focusTracker = this._register(trackFocus(this._container)); + } + + private _dimension?: Dimension; + + private _relayout() { + if (this._dimension) { + this._doLayout(this._inlineChatWidget.getHeight()); + } + } + + private _doLayout(heightInPixel: number) { + const width = Math.min(640, this._terminalElement.clientWidth - 12/* padding */ - 2/* border */ - Constants.HorizontalMargin); + const height = Math.min(480, heightInPixel, this._getTerminalWrapperHeight() ?? Number.MAX_SAFE_INTEGER); + if (width === 0 || height === 0) { + return; + } + this._dimension = new Dimension(width, height); + this._inlineChatWidget.layout(this._dimension); + this._updateVerticalPosition(); + } + + private _reset() { + this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); + this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); + } + + reveal(): void { + this._doLayout(this._inlineChatWidget.getHeight()); + this._container.classList.remove('hide'); + this._focusedContextKey.set(true); + this._visibleContextKey.set(true); + this._inlineChatWidget.focus(); + } + + private _updateVerticalPosition(): void { + const font = this._instance.xterm?.getFont(); + if (!font?.charHeight) { + return; + } + const terminalWrapperHeight = this._getTerminalWrapperHeight() ?? 0; + const cellHeight = font.charHeight * font.lineHeight; + const topPadding = terminalWrapperHeight - (this._instance.rows * cellHeight); + const cursorY = (this._instance.xterm?.raw.buffer.active.cursorY ?? 0) + 1; + const top = topPadding + cursorY * cellHeight; + this._container.style.top = `${top}px`; + const widgetHeight = this._inlineChatWidget.getHeight(); + if (!terminalWrapperHeight) { + return; + } + if (top > terminalWrapperHeight - widgetHeight) { + this._container.style.top = ''; + } + } + + private _getTerminalWrapperHeight(): number | undefined { + return this._terminalElement.clientHeight; + } + + hide(): void { + this._container.classList.add('hide'); + this._reset(); + this._inlineChatWidget.updateChatMessage(undefined); + this._inlineChatWidget.updateFollowUps(undefined); + this._inlineChatWidget.updateProgress(false); + this._inlineChatWidget.updateToolbar(false); + this._focusedContextKey.set(false); + this._visibleContextKey.set(false); + this._inlineChatWidget.value = ''; + this._instance.focus(); + } + focus(): void { + this._inlineChatWidget.focus(); + } + focusResponse(): void { + const responseElement = this._inlineChatWidget.domNode.querySelector(ChatElementSelectors.ResponseEditor) || this._inlineChatWidget.domNode.querySelector(ChatElementSelectors.ResponseMessage); + if (responseElement instanceof HTMLElement) { + responseElement.focus(); + } + } + hasFocus(): boolean { + return this._inlineChatWidget.hasFocus(); + } + input(): string { + return this._inlineChatWidget.value; + } + setValue(value?: string) { + this._inlineChatWidget.value = value ?? ''; + } + acceptCommand(code: string, shouldExecute: boolean): void { + this._instance.runCommand(code, shouldExecute); + this.hide(); + } + + updateProgress(progress?: IChatProgress): void { + this._inlineChatWidget.updateProgress(progress?.kind === 'content' || progress?.kind === 'markdownContent'); + } + public get focusTracker(): IFocusTracker { + return this._focusTracker; + } +} + +const enum ChatElementSelectors { + ResponseEditor = '.chatMessageContent textarea', + ResponseMessage = '.chatMessageContent', +} diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts index b680864e23d..389f40afc6e 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts @@ -83,6 +83,11 @@ export interface ITerminalSimpleLink { */ uri?: URI; + /** + * An optional full line to be used for context when resolving. + */ + contextLine?: string; + /** * The location or selection range of the link. */ diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts index 0327aa48168..2a0269486a3 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts @@ -83,7 +83,7 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr }); } const links = await this._getLinks(); - return await this._terminalLinkQuickpick.show(links); + return await this._terminalLinkQuickpick.show(this._instance, links); } private async _getLinks(): Promise<{ viewport: IDetectedLinks; all: Promise }> { diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 8d981fb62d2..5d8f803de51 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -441,6 +441,6 @@ export interface ILineColumnInfo { export interface IDetectedLinks { wordLinks?: ILink[]; webLinks?: ILink[]; - fileLinks?: ILink[]; + fileLinks?: (ILink | TerminalLink)[]; folderLinks?: ILink[]; } diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index 6b5af707012..d4247bbd835 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts @@ -22,7 +22,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; +import { detectLinks, getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { @@ -98,10 +98,27 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { async open(link: ITerminalSimpleLink): Promise { const osPath = osPathModule(this._getOS()); const pathSeparator = osPath.sep; + // Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format let text = link.text.replace(/^file:\/\/\/?/, ''); text = osPath.normalize(text).replace(/^(\.+[\\/])+/, ''); + // Try extract any trailing line and column numbers by matching the text against parsed + // links. This will give a search link `foo` on a line like `"foo", line 10` to open the + // quick pick with `foo:10` as the contents. + if (link.contextLine) { + const parsedLinks = detectLinks(link.contextLine, this._getOS()); + const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text === parsedLink.path.text); + if (matchingParsedLink) { + if (matchingParsedLink.suffix?.row !== undefined) { + text += `:${matchingParsedLink.suffix.row}`; + if (matchingParsedLink.suffix?.col !== undefined) { + text += `:${matchingParsedLink.suffix.col}`; + } + } + } + } + // Remove `:` from the end of the link. // Examples: // - Ruby stack traces: :in ... diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts index 8328315b957..94dbbd2f5cd 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts @@ -277,6 +277,11 @@ function detectLinksViaSuffix(line: string): IParsedLink[] { }; path = path.substring(prefix.text.length); + // Don't allow suffix links to be returned when the link itself is the empty string + if (path.trim().length === 0) { + continue; + } + // If there are multiple characters in the prefix, trim the prefix if the _first_ // suffix character is the same as the last prefix character. For example, for the // text `echo "'foo' on line 1"`: diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts index 0cea7b3f514..423aa2430b2 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts @@ -8,46 +8,54 @@ import { Emitter, Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { QuickPickItem, IQuickInputService, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { IDetectedLinks } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager'; -import { TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalLinkQuickPickEvent, type IDetachedTerminalInstance, type ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import type { ILink } from '@xterm/xterm'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import type { TerminalLink } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLink'; -import { Sequencer } from 'vs/base/common/async'; -import { EditorViewState } from 'vs/workbench/browser/quickaccess'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { Sequencer, timeout } from 'vs/base/common/async'; +import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; import { getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminalContrib/links/browser/links'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import type { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class TerminalLinkQuickpick extends DisposableStore { private readonly _editorSequencer = new Sequencer(); - private readonly _editorViewState: EditorViewState; + private readonly _editorViewState: PickerEditorState; + + private _instance: ITerminalInstance | IDetachedTerminalInstance | undefined; private readonly _onDidRequestMoreLinks = this.add(new Emitter()); readonly onDidRequestMoreLinks = this._onDidRequestMoreLinks.event; constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IEditorService private readonly _editorService: IEditorService, - @IHistoryService private readonly _historyService: IHistoryService, + @ILabelService private readonly _labelService: ILabelService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IInstantiationService instantiationService: IInstantiationService ) { super(); - this._editorViewState = new EditorViewState(_editorService); + this._editorViewState = this.add(instantiationService.createInstance(PickerEditorState)); } - async show(links: { viewport: IDetectedLinks; all: Promise }): Promise { + async show(instance: ITerminalInstance | IDetachedTerminalInstance, links: { viewport: IDetectedLinks; all: Promise }): Promise { + this._instance = instance; + + // Allow all links a small amount of time to elapse to finish, if this is not done in this + // time they will be loaded upon the first filter. + const result = await Promise.race([links.all, timeout(500)]); + const usingAllLinks = typeof result === 'object'; + const resolvedLinks = usingAllLinks ? result : links.viewport; + // Get raw link picks - const wordPicks = links.viewport.wordLinks ? await this._generatePicks(links.viewport.wordLinks) : undefined; - const filePicks = links.viewport.fileLinks ? await this._generatePicks(links.viewport.fileLinks) : undefined; - const folderPicks = links.viewport.folderLinks ? await this._generatePicks(links.viewport.folderLinks) : undefined; - const webPicks = links.viewport.webLinks ? await this._generatePicks(links.viewport.webLinks) : undefined; + const wordPicks = resolvedLinks.wordLinks ? await this._generatePicks(resolvedLinks.wordLinks) : undefined; + const filePicks = resolvedLinks.fileLinks ? await this._generatePicks(resolvedLinks.fileLinks) : undefined; + const folderPicks = resolvedLinks.folderLinks ? await this._generatePicks(resolvedLinks.folderLinks) : undefined; + const webPicks = resolvedLinks.webLinks ? await this._generatePicks(resolvedLinks.webLinks) : undefined; const picks: LinkQuickPickItem[] = []; if (webPicks) { @@ -81,36 +89,38 @@ export class TerminalLinkQuickpick extends DisposableStore { // ASAP with only the viewport entries. let accepted = false; const disposables = new DisposableStore(); - disposables.add(Event.once(pick.onDidChangeValue)(async () => { - const allLinks = await links.all; - if (accepted) { - return; - } - const wordIgnoreLinks = [...(allLinks.fileLinks ?? []), ...(allLinks.folderLinks ?? []), ...(allLinks.webLinks ?? [])]; - - const wordPicks = allLinks.wordLinks ? await this._generatePicks(allLinks.wordLinks, wordIgnoreLinks) : undefined; - const filePicks = allLinks.fileLinks ? await this._generatePicks(allLinks.fileLinks) : undefined; - const folderPicks = allLinks.folderLinks ? await this._generatePicks(allLinks.folderLinks) : undefined; - const webPicks = allLinks.webLinks ? await this._generatePicks(allLinks.webLinks) : undefined; - const picks: LinkQuickPickItem[] = []; - if (webPicks) { - picks.push({ type: 'separator', label: localize('terminal.integrated.urlLinks', "Url") }); - picks.push(...webPicks); - } - if (filePicks) { - picks.push({ type: 'separator', label: localize('terminal.integrated.localFileLinks', "File") }); - picks.push(...filePicks); - } - if (folderPicks) { - picks.push({ type: 'separator', label: localize('terminal.integrated.localFolderLinks', "Folder") }); - picks.push(...folderPicks); - } - if (wordPicks) { - picks.push({ type: 'separator', label: localize('terminal.integrated.searchLinks', "Workspace Search") }); - picks.push(...wordPicks); - } - pick.items = picks; - })); + if (!usingAllLinks) { + disposables.add(Event.once(pick.onDidChangeValue)(async () => { + const allLinks = await links.all; + if (accepted) { + return; + } + const wordIgnoreLinks = [...(allLinks.fileLinks ?? []), ...(allLinks.folderLinks ?? []), ...(allLinks.webLinks ?? [])]; + + const wordPicks = allLinks.wordLinks ? await this._generatePicks(allLinks.wordLinks, wordIgnoreLinks) : undefined; + const filePicks = allLinks.fileLinks ? await this._generatePicks(allLinks.fileLinks) : undefined; + const folderPicks = allLinks.folderLinks ? await this._generatePicks(allLinks.folderLinks) : undefined; + const webPicks = allLinks.webLinks ? await this._generatePicks(allLinks.webLinks) : undefined; + const picks: LinkQuickPickItem[] = []; + if (webPicks) { + picks.push({ type: 'separator', label: localize('terminal.integrated.urlLinks', "Url") }); + picks.push(...webPicks); + } + if (filePicks) { + picks.push({ type: 'separator', label: localize('terminal.integrated.localFileLinks', "File") }); + picks.push(...filePicks); + } + if (folderPicks) { + picks.push({ type: 'separator', label: localize('terminal.integrated.localFolderLinks', "Folder") }); + picks.push(...folderPicks); + } + if (wordPicks) { + picks.push({ type: 'separator', label: localize('terminal.integrated.searchLinks', "Workspace Search") }); + picks.push(...wordPicks); + } + pick.items = picks; + })); + } disposables.add(pick.onDidChangeActive(async () => { const [item] = pick.activeItems; @@ -119,12 +129,23 @@ export class TerminalLinkQuickpick extends DisposableStore { return new Promise(r => { disposables.add(pick.onDidHide(({ reason }) => { + + // Restore terminal scroll state + if (this._terminalScrollStateSaved) { + const markTracker = this._instance?.xterm?.markTracker; + if (markTracker) { + markTracker.restoreScrollState(); + markTracker.clear(); + this._terminalScrollStateSaved = false; + } + } + // Restore view state upon cancellation if we changed it // but only when the picker was closed via explicit user // gesture and not e.g. when focus was lost because that // could mean the user clicked into the editor directly. if (reason === QuickInputHideReason.Gesture) { - this._editorViewState.restore(true); + this._editorViewState.restore(); } disposables.dispose(); if (pick.selectedItems.length === 0) { @@ -133,6 +154,16 @@ export class TerminalLinkQuickpick extends DisposableStore { r(); })); disposables.add(Event.once(pick.onDidAccept)(() => { + // Restore terminal scroll state + if (this._terminalScrollStateSaved) { + const markTracker = this._instance?.xterm?.markTracker; + if (markTracker) { + markTracker.restoreScrollState(); + markTracker.clear(); + this._terminalScrollStateSaved = false; + } + } + accepted = true; const event = new TerminalLinkQuickPickEvent(EventType.CLICK); const activeItem = pick.activeItems?.[0]; @@ -148,38 +179,83 @@ export class TerminalLinkQuickpick extends DisposableStore { /** * @param ignoreLinks Links with labels to not include in the picks. */ - private async _generatePicks(links: ILink[], ignoreLinks?: ILink[]): Promise { + private async _generatePicks(links: (ILink | TerminalLink)[], ignoreLinks?: ILink[]): Promise { if (!links) { return; } - const linkKeys: Set = new Set(); + const linkTextKeys: Set = new Set(); + const linkUriKeys: Set = new Set(); const picks: ITerminalLinkQuickPickItem[] = []; for (const link of links) { - const label = link.text; - if (!linkKeys.has(label) && (!ignoreLinks || !ignoreLinks.some(e => e.text === label))) { - linkKeys.add(label); - picks.push({ label, link }); + let label = link.text; + if (!linkTextKeys.has(label) && (!ignoreLinks || !ignoreLinks.some(e => e.text === label))) { + linkTextKeys.add(label); + + // Add a consistently formatted resolved URI label to the description if applicable + let description: string | undefined; + if ('uri' in link && link.uri) { + // For local files and folders, mimic the presentation of go to file + if ( + link.type === TerminalBuiltinLinkType.LocalFile || + link.type === TerminalBuiltinLinkType.LocalFolderInWorkspace || + link.type === TerminalBuiltinLinkType.LocalFolderOutsideWorkspace + ) { + label = basenameOrAuthority(link.uri); + description = this._labelService.getUriLabel(dirname(link.uri), { relative: true }); + } + + // Add line and column numbers to the label if applicable + if (link.type === TerminalBuiltinLinkType.LocalFile) { + if (link.parsedLink?.suffix?.row !== undefined) { + label += `:${link.parsedLink.suffix.row}`; + if (link.parsedLink?.suffix?.rowEnd !== undefined) { + label += `-${link.parsedLink.suffix.rowEnd}`; + } + if (link.parsedLink?.suffix?.col !== undefined) { + label += `:${link.parsedLink.suffix.col}`; + if (link.parsedLink?.suffix?.colEnd !== undefined) { + label += `-${link.parsedLink.suffix.colEnd}`; + } + } + } + } + + // Skip the link if it's a duplicate URI + line/col + if (description) { + if (linkUriKeys.has(label + '|' + description)) { + continue; + } + linkUriKeys.add(label + '|' + description); + } + } + + picks.push({ label, link, description }); } } return picks.length > 0 ? picks : undefined; } private _previewItem(item: ITerminalLinkQuickPickItem | IQuickPickItem) { - if (!item || !('link' in item) || !item.link || !('uri' in item.link) || !item.link.uri) { + if (!item || !('link' in item) || !item.link) { return; } + // Any link can be previewed in the termninal const link = item.link; - if (link.type !== TerminalBuiltinLinkType.LocalFile) { + this._previewItemInTerminal(link); + + if (!('uri' in link) || !link.uri) { return; } - // Don't open if preview editors are disabled as it may open many editor - const config = this._configurationService.getValue(); - if (!config.workbench?.editor?.enablePreview) { + if (link.type !== TerminalBuiltinLinkType.LocalFile) { return; } + this._previewItemInEditor(link); + } + + private _previewItemInEditor(link: TerminalLink) { const linkSuffix = link.parsedLink ? link.parsedLink.suffix : getLinkSuffix(link.text); const selection = linkSuffix?.row === undefined ? undefined : { startLineNumber: linkSuffix.row ?? 1, @@ -190,18 +266,25 @@ export class TerminalLinkQuickpick extends DisposableStore { this._editorViewState.set(); this._editorSequencer.queue(async () => { - // disable and re-enable history service so that we can ignore this history entry - const disposable = this._historyService.suspendTracking(); - try { - await this._editorService.openEditor({ - resource: link.uri, - options: { preserveFocus: true, revealIfOpened: true, ignoreError: true, selection, } - }); - } finally { - disposable.dispose(); - } + await this._editorViewState.openTransientEditor({ + resource: link.uri, + options: { preserveFocus: true, revealIfOpened: true, ignoreError: true, selection, } + }); }); } + + private _terminalScrollStateSaved: boolean = false; + private _previewItemInTerminal(link: ILink) { + const xterm = this._instance?.xterm; + if (!xterm) { + return; + } + if (!this._terminalScrollStateSaved) { + xterm.markTracker.saveScrollState(); + this._terminalScrollStateSaved = true; + } + xterm.markTracker.revealRange(link.range); + } } export interface ITerminalLinkQuickPickItem extends IQuickPickItem { diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts index 259df08a249..adee3ee9f1f 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts @@ -37,6 +37,8 @@ const enum Constants { const fallbackMatchers: RegExp[] = [ // Python style error: File "", line /^ *File (?"(?.+)"(, line (?\d+))?)/, + // Unknown tool #200166: FILE :: + /^ +FILE +(?(?.+)(?::(?\d+)(?::(?\d+))?)?)/, // Some C++ compile error formats: // C:\foo\bar baz(339) : error ... // C:\foo\bar baz(339,12) : error ... diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts index 1675d0fac73..02a9b2cc39f 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts @@ -103,7 +103,8 @@ export class TerminalWordLinkDetector extends Disposable implements ITerminalLin links.push({ text: word.text, bufferRange, - type: TerminalBuiltinLinkType.Search + type: TerminalBuiltinLinkType.Search, + contextLine: text }); } diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts index 71d641d0739..04e4aeb5e9b 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts @@ -706,5 +706,11 @@ suite('TerminalLinkParsing', () => { }); } }); + suite('should ignore links with suffixes when the path itself is the empty string', () => { + deepStrictEqual( + detectLinks('""",1', OperatingSystem.Linux), + [] as IParsedLink[] + ); + }); }); }); diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index faae5d685ee..785ad9658ab 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -138,6 +138,10 @@ const supportedFallbackLinkFormats: LinkFormatInfo[] = [ // Python style error: File "", line { urlFormat: 'File "{0}"', linkCellStartOffset: 5 }, { urlFormat: 'File "{0}", line {1}', line: '5', linkCellStartOffset: 5 }, + // Unknown tool #200166: FILE :: + { urlFormat: ' FILE {0}', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}', line: '5', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}:{2}', line: '5', column: '3', linkCellStartOffset: 7 }, // Some C++ compile error formats { urlFormat: '{0}({1}) :', line: '5', linkCellEndOffset: -2 }, { urlFormat: '{0}({1},{2}) :', line: '5', column: '3', linkCellEndOffset: -2 }, diff --git a/code/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/code/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 4dc0908e54e..087576f6ca2 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -8,7 +8,7 @@ import type { IBufferLine, IMarker, ITerminalOptions, ITheme, Terminal as RawXte import { importAMDNodeModule } from 'vs/amdX'; import { $, addDisposableListener, addStandardDisposableListener, getWindow } from 'vs/base/browser/dom'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; -import { debounce, memoize, throttle } from 'vs/base/common/decorators'; +import { memoize, throttle } from 'vs/base/common/decorators'; import { Event } from 'vs/base/common/event'; import { Disposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; @@ -112,6 +112,10 @@ export class TerminalStickyScrollOverlay extends Disposable { this._register(this._themeService.onDidColorThemeChange(() => { this._syncOptions(); })); + this._register(this._xterm.raw.onResize(() => { + this._syncOptions(); + this._throttledRefresh(); + })); this._getSerializeAddonConstructor().then(SerializeAddon => { this._serializeAddon = this._register(new SerializeAddon()); @@ -150,7 +154,7 @@ export class TerminalStickyScrollOverlay extends Disposable { this._xterm.raw.onLineFeed, // Rarely an update may be required after just a cursor move, like when // scrolling horizontally in a pager - this._xterm.raw.onCursorMove + this._xterm.raw.onCursorMove, )(() => this._refresh()), addStandardDisposableListener(this._xterm.raw.element!.querySelector('.xterm-viewport')!, 'scroll', () => this._refresh()), ); @@ -193,17 +197,18 @@ export class TerminalStickyScrollOverlay extends Disposable { if (command && this._currentStickyCommand !== command) { this._throttledRefresh(); } else { - this._debouncedRefresh(); + // If it's the same command, do not throttle as the sticky scroll overlay height may + // need to be adjusted. This would cause a flicker if throttled. + this._refreshNow(); } } - @debounce(20) - private _debouncedRefresh(): void { - this._throttledRefresh(); - } - @throttle(0) private _throttledRefresh(): void { + this._refreshNow(); + } + + private _refreshNow(): void { const command = this._commandDetection.getCommandForLine(this._xterm.raw.buffer.active.viewportY); // The command from viewportY + 1 is used because this one will not be obscured by sticky @@ -245,6 +250,12 @@ export class TerminalStickyScrollOverlay extends Disposable { return; } + // Hide sticky scroll if the prompt has been trimmed from the buffer + if (command.promptStartMarker?.line === -1) { + this._setVisible(false); + return; + } + // Determine sticky scroll line count const buffer = xterm.buffer.active; const promptRowCount = command.getPromptRowCount(); @@ -281,7 +292,7 @@ export class TerminalStickyScrollOverlay extends Disposable { } } - // Clear attrs, reset cursor position, clear right + // Get the line content of the command from the terminal const content = this._serializeAddon.serialize({ range: { start: stickyScrollLineStart + rowOffset, @@ -297,8 +308,13 @@ export class TerminalStickyScrollOverlay extends Disposable { } // Write content if it differs - if (content && this._currentContent !== content) { + if ( + content && this._currentContent !== content || + this._stickyScrollOverlay.cols !== xterm.cols || + this._stickyScrollOverlay.rows !== stickyScrollLineCount + ) { this._stickyScrollOverlay.resize(this._stickyScrollOverlay.cols, stickyScrollLineCount); + // Clear attrs, reset cursor position, clear right this._stickyScrollOverlay.write('\x1b[0m\x1b[H\x1b[2J'); this._stickyScrollOverlay.write(content); this._currentContent = content; @@ -317,7 +333,18 @@ export class TerminalStickyScrollOverlay extends Disposable { const termBox = xterm.element.getBoundingClientRect(); const rowHeight = termBox.height / xterm.rows; const overlayHeight = stickyScrollLineCount * rowHeight; - this._element.style.bottom = `${termBox.height - overlayHeight + 1}px`; + + // Adjust sticky scroll content if it would below the end of the command, obscuring the + // following command. + let endMarkerOffset = 0; + if (!isPartialCommand && command.endMarker && command.endMarker.line !== -1) { + if (buffer.viewportY + stickyScrollLineCount > command.endMarker.line) { + const diff = buffer.viewportY + stickyScrollLineCount - command.endMarker.line; + endMarkerOffset = diff * rowHeight; + } + } + + this._element.style.bottom = `${termBox.height - overlayHeight + 1 + endMarkerOffset}px`; } } else { this._setVisible(false); @@ -370,12 +397,15 @@ export class TerminalStickyScrollOverlay extends Disposable { // Scroll to the command on click this._register(addStandardDisposableListener(hoverOverlay, 'click', () => { - if (this._xterm && this._currentStickyCommand && 'getOutput' in this._currentStickyCommand) { + if (this._xterm && this._currentStickyCommand) { this._xterm.markTracker.revealCommand(this._currentStickyCommand); this._instance.focus(); } })); + // Forward mouse events to the terminal + this._register(addStandardDisposableListener(hoverOverlay, 'wheel', e => this._xterm?.raw.element?.dispatchEvent(new WheelEvent(e.type, e)))); + // Context menu - stop propagation on mousedown because rightClickBehavior listens on // mousedown, not contextmenu this._register(addDisposableListener(hoverOverlay, 'mousedown', e => { diff --git a/code/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/code/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index d96710fbd0d..6a11a647a6c 100644 --- a/code/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/code/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -81,7 +81,13 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri return; } - return report.getUri(model.uri); + const file = report.getUri(model.uri); + if (file) { + return file; + } + + report.didAddCoverage.read(reader); // re-read if changes when there's no report + return undefined; }); this._register(autorun(reader => { diff --git a/code/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts b/code/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts index d7f385457ae..33060d117ea 100644 --- a/code/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts +++ b/code/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts @@ -253,21 +253,20 @@ export class TreeProjection extends Disposable implements ITestTreeProjection { * @inheritdoc */ public applyTo(tree: ObjectTree) { - for (const s of [this.changedParents, this.resortedParents]) { - for (const element of s) { - if (element && !tree.hasElement(element)) { - s.delete(element); - } - } - } - for (const parent of this.changedParents) { - tree.setChildren(parent, getChildrenForParent(this.lastState, this.rootsWithChildren, parent), { diffIdentityProvider: testIdentityProvider }); + if (!parent || tree.hasElement(parent)) { + tree.setChildren(parent, getChildrenForParent(this.lastState, this.rootsWithChildren, parent), { diffIdentityProvider: testIdentityProvider }); + } } for (const parent of this.resortedParents) { - tree.resort(parent, false); + if (!parent || tree.hasElement(parent)) { + tree.resort(parent, false); + } } + + this.changedParents.clear(); + this.resortedParents.clear(); } /** diff --git a/code/src/vs/workbench/contrib/testing/browser/media/testMessageColorizer.css b/code/src/vs/workbench/contrib/testing/browser/media/testMessageColorizer.css new file mode 100644 index 00000000000..8c3dbab4162 --- /dev/null +++ b/code/src/vs/workbench/contrib/testing/browser/media/testMessageColorizer.css @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.test-output-peek-message-container { + .tstm-ansidec-1 { + font-weight: bold; + } + .tstm-ansidec-2 { + opacity: 0.7 + } + .tstm-ansidec-3 { + font-style: italic; + } + .tstm-ansidec-4 { + text-decoration: underline; + } + + .tstm-ansidec-fg30 { color: var(--vscode-terminal-ansiBlack); } + .tstm-ansidec-fg31 { color: var(--vscode-terminal-ansiRed); } + .tstm-ansidec-fg32 { color: var(--vscode-terminal-ansiGreen); } + .tstm-ansidec-fg33 { color: var(--vscode-terminal-ansiYellow); } + .tstm-ansidec-fg34 { color: var(--vscode-terminal-ansiBlue); } + .tstm-ansidec-fg35 { color: var(--vscode-terminal-ansiMagenta); } + .tstm-ansidec-fg36 { color: var(--vscode-terminal-ansiCyan); } + .tstm-ansidec-fg37 { color: var(--vscode-terminal-ansiWhite); } + + .tstm-ansidec-fg90 { color: var(--vscode-terminal-ansiBrightBlack); } + .tstm-ansidec-fg91 { color: var(--vscode-terminal-ansiBrightRed); } + .tstm-ansidec-fg92 { color: var(--vscode-terminal-ansiBrightGreen); } + .tstm-ansidec-fg93 { color: var(--vscode-terminal-ansiBrightYellow); } + .tstm-ansidec-fg94 { color: var(--vscode-terminal-ansiBrightBlue); } + .tstm-ansidec-fg95 { color: var(--vscode-terminal-ansiBrightMagenta); } + .tstm-ansidec-fg96 { color: var(--vscode-terminal-ansiBrightCyan); } + .tstm-ansidec-fg97 { color: var(--vscode-terminal-ansiBrightWhite); } + + .tstm-ansidec-bg30 { background-color: var(--vscode-terminal-ansiBlack); } + .tstm-ansidec-bg31 { background-color: var(--vscode-terminal-ansiRed); } + .tstm-ansidec-bg32 { background-color: var(--vscode-terminal-ansiGreen); } + .tstm-ansidec-bg33 { background-color: var(--vscode-terminal-ansiYellow); } + .tstm-ansidec-bg34 { background-color: var(--vscode-terminal-ansiBlue); } + .tstm-ansidec-bg35 { background-color: var(--vscode-terminal-ansiMagenta); } + .tstm-ansidec-bg36 { background-color: var(--vscode-terminal-ansiCyan); } + .tstm-ansidec-bg37 { background-color: var(--vscode-terminal-ansiWhite); } + + .tstm-ansidec-bg100 { background-color: var(--vscode-terminal-ansiBrightBlack); } + .tstm-ansidec-bg101 { background-color: var(--vscode-terminal-ansiBrightRed); } + .tstm-ansidec-bg102 { background-color: var(--vscode-terminal-ansiBrightGreen); } + .tstm-ansidec-bg103 { background-color: var(--vscode-terminal-ansiBrightYellow); } + .tstm-ansidec-bg104 { background-color: var(--vscode-terminal-ansiBrightBlue); } + .tstm-ansidec-bg105 { background-color: var(--vscode-terminal-ansiBrightMagenta); } + .tstm-ansidec-bg106 { background-color: var(--vscode-terminal-ansiBrightCyan); } + .tstm-ansidec-bg107 { background-color: var(--vscode-terminal-ansiBrightWhite); } +} diff --git a/code/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts b/code/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts index 7261e5e1021..56d486699a6 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { h } from 'vs/base/browser/dom'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ICustomHover, ITooltipMarkdownString, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, ITooltipMarkdownString, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { assertNever } from 'vs/base/common/assert'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; diff --git a/code/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/code/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index 2451613400f..17cfc93ca40 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -8,6 +8,7 @@ import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeNode, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; +import { findLast } from 'vs/base/common/arraysFind'; import { assertNever } from 'vs/base/common/assert'; import { Codicon } from 'vs/base/common/codicons'; import { memoize } from 'vs/base/common/decorators'; @@ -42,6 +43,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { testingStatesToIcons, testingWasCovered } from 'vs/workbench/contrib/testing/browser/icons'; import { CoverageBarSource, ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; import { ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; import { CoverageDetails, DetailType, ICoveredCount, IDeclarationCoverage, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -198,6 +200,7 @@ const shouldShowDeclDetailsOnExpand = (c: CoverageTreeElement): c is IPrefixTree class TestCoverageTree extends Disposable { private readonly tree: WorkbenchCompressibleObjectTree; + private readonly inputDisposables = this._register(new DisposableStore()); constructor( container: HTMLElement, @@ -294,6 +297,8 @@ class TestCoverageTree extends Disposable { } public setInput(coverage: TestCoverage) { + this.inputDisposables.clear(); + const files = []; for (let node of coverage.tree.nodes) { // when showing initial children, only show from the first file or tee @@ -315,6 +320,17 @@ class TestCoverageTree extends Disposable { }; }; + this.inputDisposables.add(onObservableChange(coverage.didAddCoverage, nodes => { + const toRender = findLast(nodes, n => this.tree.hasElement(n)); + if (toRender) { + this.tree.setChildren( + toRender, + Iterable.map(toRender.children?.values() || [], toChild), + { diffIdentityProvider: { getId: el => (el as TestCoverageFileNode).value!.id } } + ); + } + })); + this.tree.setChildren(null, Iterable.map(files, toChild)); } @@ -416,6 +432,7 @@ interface FileTemplateData { container: HTMLElement; bars: ManagedTestCoverageBars; templateDisposables: DisposableStore; + elementsDisposables: DisposableStore; label: IResourceLabel; } @@ -440,6 +457,7 @@ class FileCoverageRenderer implements ICompressibleTreeRenderer basenameOrAuthority((e as TestCoverageFileNode).value!.uri)) : basenameOrAuthority(file.uri); + templateData.elementsDisposables.add(autorun(reader => { + stat.value?.didChange.read(reader); + templateData.bars.setCoverageInfo(file); + })); templateData.bars.setCoverageInfo(file); templateData.label.setResource({ resource: file.uri, name }, { diff --git a/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index f6f451bcd0d..103efb04a9c 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -9,7 +9,8 @@ import { Iterable } from 'vs/base/common/iterator'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -981,15 +982,20 @@ abstract class ExecuteTestAtCursor extends Action2 { * @override */ public async run(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); const editorService = accessor.get(IEditorService); const activeEditorPane = editorService.activeEditorPane; - const activeControl = editorService.activeTextEditorControl; - if (!activeEditorPane || !activeControl) { + let editor = codeEditorService.getActiveCodeEditor(); + if (!activeEditorPane || !editor) { return; } - const position = activeControl?.getPosition(); - const model = activeControl?.getModel(); + if (editor instanceof EmbeddedCodeEditorWidget) { + editor = editor.getParentEditor(); + } + + const position = editor?.getPosition(); + const model = editor?.getModel(); if (!position || !model || !('uri' in model)) { return; } @@ -1053,8 +1059,8 @@ abstract class ExecuteTestAtCursor extends Action2 { group: this.group, tests: bestNodes.length ? bestNodes : bestNodesBefore, }); - } else if (isCodeEditor(activeControl)) { - MessageController.get(activeControl)?.showMessage(localize('noTestsAtCursor', "No tests found here"), position); + } else if (editor) { + MessageController.get(editor)?.showMessage(localize('noTestsAtCursor', "No tests found here"), position); } } } @@ -1186,9 +1192,15 @@ abstract class ExecuteTestsInCurrentFile extends Action2 { * @override */ public run(accessor: ServicesAccessor) { - const control = accessor.get(IEditorService).activeTextEditorControl; - const position = control?.getPosition(); - const model = control?.getModel(); + let editor = accessor.get(ICodeEditorService).getActiveCodeEditor(); + if (!editor) { + return; + } + if (editor instanceof EmbeddedCodeEditorWidget) { + editor = editor.getParentEditor(); + } + const position = editor?.getPosition(); + const model = editor?.getModel(); if (!position || !model || !('uri' in model)) { return; } @@ -1218,8 +1230,8 @@ abstract class ExecuteTestsInCurrentFile extends Action2 { }); } - if (isCodeEditor(control)) { - MessageController.get(control)?.showMessage(localize('noTestsInFile', "No tests found in this file"), position); + if (editor) { + MessageController.get(editor)?.showMessage(localize('noTestsInFile', "No tests found in this file"), position); } return undefined; diff --git a/code/src/vs/workbench/contrib/testing/browser/testMessageColorizer.ts b/code/src/vs/workbench/contrib/testing/browser/testMessageColorizer.ts new file mode 100644 index 00000000000..527d6e5cc32 --- /dev/null +++ b/code/src/vs/workbench/contrib/testing/browser/testMessageColorizer.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { GraphemeIterator, forAnsiStringParts, removeAnsiEscapeCodes } from 'vs/base/common/strings'; +import 'vs/css!./media/testMessageColorizer'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; + +const colorAttrRe = /^\x1b\[([0-9]+)m$/; + +const enum Classes { + Prefix = 'tstm-ansidec-', + ForegroundPrefix = Classes.Prefix + 'fg', + BackgroundPrefix = Classes.Prefix + 'bg', + Bold = Classes.Prefix + '1', + Faint = Classes.Prefix + '2', + Italic = Classes.Prefix + '3', + Underline = Classes.Prefix + '4', +} + +export const renderTestMessageAsText = (tm: string | IMarkdownString) => + typeof tm === 'string' ? removeAnsiEscapeCodes(tm) : renderStringAsPlaintext(tm); + + +/** + * Applies decorations based on ANSI styles from the test message in the editor. + * ANSI sequences are stripped from the text displayed in editor, and this + * re-applies their colorization. + * + * This uses decorations rather than language features because the string + * rendered in the editor lacks the ANSI codes needed to actually apply the + * colorization. + * + * Note: does not support TrueColor. + */ +export const colorizeTestMessageInEditor = (message: string, editor: CodeEditorWidget): IDisposable => { + const decos: string[] = []; + + editor.changeDecorations(changeAccessor => { + let start = new Position(1, 1); + let cls: string[] = []; + for (const part of forAnsiStringParts(message)) { + if (part.isCode) { + const colorAttr = colorAttrRe.exec(part.str)?.[1]; + if (!colorAttr) { + continue; + } + + const n = Number(colorAttr); + if (n === 0) { + cls.length = 0; + } else if (n === 22) { + cls = cls.filter(c => c !== Classes.Bold && c !== Classes.Italic); + } else if (n === 23) { + cls = cls.filter(c => c !== Classes.Italic); + } else if (n === 24) { + cls = cls.filter(c => c !== Classes.Underline); + } else if ((n >= 30 && n <= 39) || (n >= 90 && n <= 99)) { + cls = cls.filter(c => !c.startsWith(Classes.ForegroundPrefix)); + cls.push(Classes.ForegroundPrefix + colorAttr); + } else if ((n >= 40 && n <= 49) || (n >= 100 && n <= 109)) { + cls = cls.filter(c => !c.startsWith(Classes.BackgroundPrefix)); + cls.push(Classes.BackgroundPrefix + colorAttr); + } else { + cls.push(Classes.Prefix + colorAttr); + } + } else { + let line = start.lineNumber; + let col = start.column; + + const graphemes = new GraphemeIterator(part.str); + for (let i = 0; !graphemes.eol(); i += graphemes.nextGraphemeLength()) { + if (part.str[i] === '\n') { + line++; + col = 1; + } else { + col++; + } + } + + const end = new Position(line, col); + if (cls.length) { + decos.push(changeAccessor.addDecoration(Range.fromPositions(start, end), { + inlineClassName: cls.join(' '), + description: 'test-message-colorized', + })); + } + start = end; + } + } + }); + + return toDisposable(() => editor.removeDecorations(decos)); +}; diff --git a/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 6083c9af896..48f10e9210a 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { equals } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -41,6 +40,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { EditorLineNumberContextMenu, GutterActionsRegistry } from 'vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; +import { renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; import { DefaultGutterClickAction, TestingConfigKeys, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing, labelForTestInState } from 'vs/workbench/contrib/testing/common/constants'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; @@ -1085,7 +1085,7 @@ class TestMessageDecoration implements ITestDecoration { options.stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; options.collapseOnReplaceEdit = true; - let inlineText = renderStringAsPlaintext(message).replace(lineBreakRe, ' '); + let inlineText = renderTestMessageAsText(message).replace(lineBreakRe, ' '); if (inlineText.length > MAX_INLINE_MESSAGE_LENGTH) { inlineText = inlineText.slice(0, MAX_INLINE_MESSAGE_LENGTH - 1) + '…'; } diff --git a/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 8eb58e118a3..e5e8e34abfa 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -8,8 +8,8 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ActionBar, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; diff --git a/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 901d0a5d5da..3c541396db7 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; @@ -38,16 +37,16 @@ import { ICodeEditor, IDiffEditorConstructionOptions, isCodeEditor } from 'vs/ed import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditor, IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { IPeekViewService, PeekViewWidget, peekViewResultsBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; @@ -80,13 +79,13 @@ 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, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; 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 { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; +import { colorizeTestMessageInEditor, renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; import { testingMessagePeekBorder, testingPeekBorder, testingPeekHeaderBackground, testingPeekMessageHeaderBackground } from 'vs/workbench/contrib/testing/browser/theme'; import { AutoOpenPeekViewWhen, TestingConfigKeys, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; @@ -98,12 +97,19 @@ import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testPro import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChange, TestResultItemChangeReason, maxCountPriority, resultItemParents } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId } from 'vs/workbench/contrib/testing/common/testTypes'; +import { IRichLocation, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, InternalTestItem, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId, testResultStateToContextValues } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { IShowResultOptions, ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { ParsedTestUri, TestUriType, buildTestUri, parseTestUri } from 'vs/workbench/contrib/testing/common/testingUri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; + +const getMessageArgs = (test: TestResultItem, message: ITestMessage): ITestMessageMenuArgs => ({ + $mid: MarshalledId.TestMessageMenuArgs, + test: InternalTestItem.serialize(test), + message: ITestMessage.serialize(message), +}); class MessageSubject { public readonly test: ITestItem; @@ -112,6 +118,7 @@ class MessageSubject { public readonly actualUri: URI; public readonly messageUri: URI; public readonly revealLocation: IRichLocation | undefined; + public readonly context: ITestMessageMenuArgs | undefined; public get isDiffable() { return this.message.type === TestMessageType.Error && isDiffable(this.message); @@ -121,14 +128,6 @@ class MessageSubject { return this.message.type === TestMessageType.Error ? this.message.contextValue : undefined; } - public get context(): ITestMessageMenuArgs { - return { - $mid: MarshalledId.TestMessageMenuArgs, - extId: this.test.extId, - message: ITestMessage.serialize(this.message), - }; - } - 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; @@ -140,6 +139,7 @@ class MessageSubject { this.messageUri = buildTestUri({ ...parts, type: TestUriType.ResultMessage }); const message = this.message = messages[this.messageIndex]; + this.context = getMessageArgs(test, message); this.revealLocation = message.location ?? (test.item.uri && test.item.range ? { uri: test.item.uri, range: Range.lift(test.item.range) } : undefined); } } @@ -591,7 +591,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo } if (subject instanceof MessageSubject) { - alert(renderStringAsPlaintext(subject.message.message)); + alert(renderTestMessageAsText(subject.message.message)); } this.peek.value.setModel(subject); @@ -1038,7 +1038,7 @@ class TestResultsPeek extends PeekViewWidget { public async showInPlace(subject: InspectSubject) { if (subject instanceof MessageSubject) { const message = subject.message; - this.setTitle(firstLine(renderStringAsPlaintext(message.message)), stripIcons(subject.test.label)); + this.setTitle(firstLine(renderTestMessageAsText(message.message)), stripIcons(subject.test.label)); } else { this.setTitle(localize('testOutputTitle', 'Test Output')); } @@ -1318,6 +1318,7 @@ class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer } class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { + private readonly widgetDecorations = this._register(new MutableDisposable()); private readonly widget = this._register(new MutableDisposable()); private readonly model = this._register(new MutableDisposable()); private dimension?: dom.IDimension; @@ -1363,11 +1364,13 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { this.widget.value.setModel(modelRef.object.textEditorModel); this.widget.value.updateOptions(commonEditorOptions); + this.widgetDecorations.value = colorizeTestMessageInEditor(message.message, this.widget.value); } private clear() { - this.model.clear(); + this.widgetDecorations.clear(); this.widget.clear(); + this.model.clear(); } public layout(dimensions: dom.IDimension) { @@ -1745,7 +1748,10 @@ class CoverageElement implements ITreeElement { class TestCaseElement implements ITreeElement { public readonly type = 'test'; - public readonly context = this.test.item.extId; + public readonly context: ITestItemContext = { + $mid: MarshalledId.TestItemContext, + tests: [InternalTestItem.serialize(this.test)], + }; public readonly id = `${this.results.id}/${this.test.item.extId}`; public readonly description?: string; @@ -1826,13 +1832,12 @@ class TestMessageElement implements ITreeElement { } public get context(): ITestMessageMenuArgs { - return { - $mid: MarshalledId.TestMessageMenuArgs, - extId: this.test.item.extId, - message: ITestMessage.serialize(this.message), - }; + return getMessageArgs(this.test, this.message); } + public get outputSubject() { + return new TestOutputSubject(this.result, this.taskIndex, this.test); + } constructor( public readonly result: ITestResult, @@ -1854,7 +1859,7 @@ class TestMessageElement implements ITreeElement { this.id = this.uri.toString(); - const asPlaintext = renderStringAsPlaintext(m.message); + const asPlaintext = renderTestMessageAsText(m.message); const lines = count(asPlaintext.trimEnd(), '\n'); this.label = firstLine(asPlaintext); if (lines > 0) { @@ -2357,11 +2362,10 @@ class TreeActionsProvider { if (element instanceof TestCaseElement || element instanceof TestMessageElement) { contextKeys.push( [TestingContextKeys.testResultOutdated.key, element.test.retired], + [TestingContextKeys.testResultState.key, testResultStateToContextValues[element.test.ownComputedState]], ...getTestItemContextOverlay(element.test, capabilities), ); - } - if (element instanceof TestCaseElement) { const extId = element.test.item.extId; if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) { primary.push(new Action( @@ -2401,12 +2405,15 @@ class TreeActionsProvider { )); } + } + + if (element instanceof TestMessageElement) { primary.push(new Action( 'testing.outputPeek.goToFile', localize('testing.goToFile', "Go to Source"), ThemeIcon.asClassName(Codicon.goToFile), undefined, - () => this.commandService.executeCommand('vscode.revealTest', extId), + () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), )); } diff --git a/code/src/vs/workbench/contrib/testing/common/constants.ts b/code/src/vs/workbench/contrib/testing/common/constants.ts index 35d9eb5e552..8f6bb09e32a 100644 --- a/code/src/vs/workbench/contrib/testing/common/constants.ts +++ b/code/src/vs/workbench/contrib/testing/common/constants.ts @@ -18,6 +18,8 @@ export const enum Testing { ResultsPanelId = 'workbench.panel.testResults', ResultsViewId = 'workbench.panel.testResults.view', + + MessageLanguageId = 'vscodeInternalTestMessage' } export const enum TestExplorerViewMode { diff --git a/code/src/vs/workbench/contrib/testing/common/observableUtils.ts b/code/src/vs/workbench/contrib/testing/common/observableUtils.ts new file mode 100644 index 00000000000..26c6c087d78 --- /dev/null +++ b/code/src/vs/workbench/contrib/testing/common/observableUtils.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, IObserver } from 'vs/base/common/observable'; + +export function onObservableChange(observable: IObservable, callback: (value: T) => void): IDisposable { + const o: IObserver = { + beginUpdate() { }, + endUpdate() { }, + handlePossibleChange(observable) { + observable.reportChanges(); + }, + handleChange(_observable: IObservable, change: TChange) { + callback(change as any as T); + } + }; + + observable.addObserver(o); + return { + dispose() { + observable.removeObserver(o); + } + }; +} diff --git a/code/src/vs/workbench/contrib/testing/common/testCoverage.ts b/code/src/vs/workbench/contrib/testing/common/testCoverage.ts index 05d532263da..9f6de896f2d 100644 --- a/code/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/code/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -5,43 +5,79 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ResourceMap } from 'vs/base/common/map'; +import { deepClone } from 'vs/base/common/objects'; +import { ITransaction, observableSignal } from 'vs/base/common/observable'; import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { CoverageDetails, ICoveredCount, IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ICoverageAccessor { - provideFileCoverage: (token: CancellationToken) => Promise; - resolveFileCoverage: (fileIndex: number, token: CancellationToken) => Promise; + getCoverageDetails: (id: string, token: CancellationToken) => Promise; } +let incId = 0; + /** * Class that exposese coverage information for a run. */ export class TestCoverage { - private _tree?: WellDefinedPrefixTree; - - public static async load(taskId: string, accessor: ICoverageAccessor, uriIdentityService: IUriIdentityService, token: CancellationToken) { - const files = await accessor.provideFileCoverage(token); - const map = new ResourceMap(); - for (const [i, file] of files.entries()) { - map.set(file.uri, new FileCoverage(file, i, accessor)); - } - return new TestCoverage(taskId, map, uriIdentityService); - } - - public get tree() { - return this._tree ??= this.buildCoverageTree(); - } + private readonly fileCoverage = new ResourceMap(); + public readonly didAddCoverage = observableSignal[]>(this); + public readonly tree = new WellDefinedPrefixTree(); public readonly associatedData = new Map(); constructor( public readonly fromTaskId: string, - private readonly fileCoverage: ResourceMap, private readonly uriIdentityService: IUriIdentityService, + private readonly accessor: ICoverageAccessor, ) { } + public append(rawCoverage: IFileCoverage, tx: ITransaction | undefined) { + const coverage = new FileCoverage(rawCoverage, this.accessor); + const previous = this.getComputedForUri(coverage.uri); + const applyDelta = (kind: 'statement' | 'branch' | 'declaration', node: ComputedFileCoverage) => { + if (!node[kind]) { + if (coverage[kind]) { + node[kind] = { ...coverage[kind]! }; + } + } else { + node[kind]!.covered += (coverage[kind]?.covered || 0) - (previous?.[kind]?.covered || 0); + node[kind]!.total += (coverage[kind]?.total || 0) - (previous?.[kind]?.total || 0); + } + }; + + // We insert using the non-canonical path to normalize for casing differences + // between URIs, but when inserting an intermediate node always use 'a' canonical + // version. + const canonical = [...this.treePathForUri(coverage.uri, /* canonical = */ true)]; + const chain: IPrefixTreeNode[] = []; + this.tree.insert(this.treePathForUri(coverage.uri, /* canonical = */ false), coverage, node => { + chain.push(node); + + if (chain.length === canonical.length - 1) { + node.value = coverage; + } else if (!node.value) { + // clone because later intersertions can modify the counts: + const intermediate = deepClone(rawCoverage); + intermediate.id = String(incId++); + intermediate.uri = this.treePathToUri(canonical.slice(0, chain.length)); + node.value = new ComputedFileCoverage(intermediate); + } else { + applyDelta('statement', node.value); + applyDelta('branch', node.value); + applyDelta('declaration', node.value); + node.value.didChange.trigger(tx); + } + }); + + this.fileCoverage.set(coverage.uri, coverage); + if (chain) { + this.didAddCoverage.trigger(tx, chain); + } + } + /** * Gets coverage information for all files. */ @@ -64,54 +100,6 @@ export class TestCoverage { return this.tree.find(this.treePathForUri(uri, /* canonical = */ false)); } - private buildCoverageTree() { - const tree = new WellDefinedPrefixTree(); - const nodeCanonicalSegments = new Map, string>(); - - // 1. Initial iteration. We insert based on the case-erased file path, and - // then tag the nodes with their 'canonical' path segment preserving the - // original casing we were given, to avoid #200604 - for (const file of this.fileCoverage.values()) { - const keyPath = this.treePathForUri(file.uri, /* canonical = */ false); - const canonicalPath = this.treePathForUri(file.uri, /* canonical = */ true); - tree.insert(keyPath, file, node => { - nodeCanonicalSegments.set(node, canonicalPath.next().value as string); - }); - } - - // 2. Depth-first iteration to create computed nodes - const calculateComputed = (path: string[], node: IPrefixTreeNode): AbstractFileCoverage => { - if (node.value) { - return node.value; - } - - const fileCoverage: IFileCoverage = { - uri: this.treePathToUri(path), - statement: ICoveredCount.empty(), - }; - - if (node.children) { - for (const [prefix, child] of node.children) { - path.push(nodeCanonicalSegments.get(child) || prefix); - const v = calculateComputed(path, child); - path.pop(); - - ICoveredCount.sum(fileCoverage.statement, v.statement); - if (v.branch) { ICoveredCount.sum(fileCoverage.branch ??= ICoveredCount.empty(), v.branch); } - if (v.declaration) { ICoveredCount.sum(fileCoverage.declaration ??= ICoveredCount.empty(), v.declaration); } - } - } - - return node.value = new ComputedFileCoverage(fileCoverage); - }; - - for (const node of tree.nodes) { - calculateComputed([], node); - } - - return tree; - } - private *treePathForUri(uri: URI, canconicalPath: boolean) { yield uri.scheme; yield uri.authority; @@ -143,10 +131,12 @@ export const getTotalCoveragePercent = (statement: ICoveredCount, branch: ICover }; export abstract class AbstractFileCoverage { + public readonly id: string; public readonly uri: URI; - public readonly statement: ICoveredCount; - public readonly branch?: ICoveredCount; - public readonly declaration?: ICoveredCount; + public statement: ICoveredCount; + public branch?: ICoveredCount; + public declaration?: ICoveredCount; + public readonly didChange = observableSignal(this); /** * Gets the total coverage percent based on information provided. @@ -157,6 +147,7 @@ export abstract class AbstractFileCoverage { } constructor(coverage: IFileCoverage) { + this.id = coverage.id; this.uri = coverage.uri; this.statement = coverage.statement; this.branch = coverage.branch; @@ -171,7 +162,7 @@ export abstract class AbstractFileCoverage { export class ComputedFileCoverage extends AbstractFileCoverage { } export class FileCoverage extends AbstractFileCoverage { - private _details?: CoverageDetails[] | Promise; + private _details?: Promise; private resolved?: boolean; /** Gets whether details are synchronously available */ @@ -179,16 +170,15 @@ export class FileCoverage extends AbstractFileCoverage { return this._details instanceof Array || this.resolved; } - constructor(coverage: IFileCoverage, private readonly index: number, private readonly accessor: ICoverageAccessor) { + constructor(coverage: IFileCoverage, private readonly accessor: ICoverageAccessor) { super(coverage); - this._details = coverage.details; } /** * Gets per-line coverage details. */ public async details(token = CancellationToken.None) { - this._details ??= this.accessor.resolveFileCoverage(this.index, token); + this._details ??= this.accessor.getCoverageDetails(this.id, token); try { const d = await this._details; diff --git a/code/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/code/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 57c0832fdfd..0bf62937458 100644 --- a/code/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/code/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -6,16 +6,14 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IObservable, observableValue } from 'vs/base/common/observable'; -import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestRunTaskResults } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export const ITestCoverageService = createDecorator('testCoverageService'); @@ -50,7 +48,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ @IContextKeyService contextKeyService: IContextKeyService, @ITestResultService resultService: ITestResultService, @IViewsService private readonly viewsService: IViewsService, - @INotificationService private readonly notificationService: INotificationService, ) { super(); this._isOpenKey = TestingContextKeys.isTestCoverageOpen.bindTo(contextKeyService); @@ -76,21 +73,13 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ public async openCoverage(task: ITestRunTaskResults, focus = true) { this.lastOpenCts.value?.cancel(); const cts = this.lastOpenCts.value = new CancellationTokenSource(); - const getCoverage = task.coverage.get(); - if (!getCoverage) { + const coverage = task.coverage.get(); + if (!coverage) { return; } - try { - const coverage = await getCoverage(cts.token); - this.selected.set(coverage, undefined); - this._isOpenKey.set(true); - } catch (e) { - if (!cts.token.isCancellationRequested) { - this.notificationService.error(localize('testCoverageError', 'Failed to load test coverage: {0}', String(e))); - } - return; - } + this.selected.set(coverage, undefined); + this._isOpenKey.set(true); if (focus && !cts.token.isCancellationRequested) { this.viewsService.openView(Testing.CoverageViewId, true); diff --git a/code/src/vs/workbench/contrib/testing/common/testResult.ts b/code/src/vs/workbench/contrib/testing/common/testResult.ts index e6056621bf2..6bbff4a7c94 100644 --- a/code/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/code/src/vs/workbench/contrib/testing/common/testResult.ts @@ -5,14 +5,12 @@ import { DeferredPromise } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, observableValue } from 'vs/base/common/observable'; import { language } from 'vs/base/common/platform'; import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; -import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; @@ -25,7 +23,7 @@ export interface ITestRunTaskResults extends ITestRunTask { /** * Contains test coverage for the result, if it's available. */ - readonly coverage: IObservable Promise)>; + readonly coverage: IObservable; /** * Messages from the task not associated with any specific test. @@ -366,7 +364,7 @@ export class LiveTestResult extends Disposable implements ITestResult { const { offset, length } = task.output.append(output, marker); const message: ITestOutputMessage = { location, - message: removeAnsiEscapeCodes(preview), + message: preview, offset, length, marker, diff --git a/code/src/vs/workbench/contrib/testing/common/testTypes.ts b/code/src/vs/workbench/contrib/testing/common/testTypes.ts index 7737ef78020..620bba113d6 100644 --- a/code/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/code/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -21,6 +21,16 @@ export const enum TestResultState { Errored = 6 } +export const testResultStateToContextValues: { [K in TestResultState]: string } = { + [TestResultState.Unset]: 'unset', + [TestResultState.Queued]: 'queued', + [TestResultState.Running]: 'running', + [TestResultState.Passed]: 'passed', + [TestResultState.Failed]: 'failed', + [TestResultState.Skipped]: 'skipped', + [TestResultState.Errored]: 'errored', +}; + /** note: keep in sync with TestRunProfileKind in vscode.d.ts */ export const enum ExtTestRunProfileKind { Run = 1, @@ -546,36 +556,35 @@ export namespace ICoveredCount { } export interface IFileCoverage { + id: string; uri: URI; statement: ICoveredCount; branch?: ICoveredCount; declaration?: ICoveredCount; - details?: CoverageDetails[]; } - export namespace IFileCoverage { export interface Serialized { + id: string; uri: UriComponents; statement: ICoveredCount; branch?: ICoveredCount; declaration?: ICoveredCount; - details?: CoverageDetails.Serialized[]; } export const serialize = (original: Readonly): Serialized => ({ + id: original.id, statement: original.statement, branch: original.branch, declaration: original.declaration, - details: original.details?.map(CoverageDetails.serialize), uri: original.uri.toJSON(), }); export const deserialize = (uriIdentity: ITestUriCanonicalizer, serialized: Serialized): IFileCoverage => ({ + id: serialized.id, statement: serialized.statement, branch: serialized.branch, declaration: serialized.declaration, - details: serialized.details?.map(CoverageDetails.deserialize), uri: uriIdentity.asCanonicalUri(URI.revive(serialized.uri)), }); } @@ -755,7 +764,7 @@ export interface ITestMessageMenuArgs { /** Marshalling marker */ $mid: MarshalledId.TestMessageMenuArgs; /** Tests ext ID */ - extId: string; + test: InternalTestItem.Serialized; /** Serialized test message */ message: ITestMessage.Serialized; } diff --git a/code/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/code/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index 2509bbd2c12..8f4dab17dc1 100644 --- a/code/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/code/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -119,7 +119,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode const content = result.tasks[parsed.taskIndex].output.getRange(message.offset, message.length); text = removeAnsiEscapeCodes(content.toString()); } else if (typeof message.message === 'string') { - text = message.message; + text = removeAnsiEscapeCodes(message.message); } else { text = message.message.value; language = this.languageService.createById('markdown'); diff --git a/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index cc7821a4e27..ddef4fcdc15 100644 --- a/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -67,4 +67,8 @@ export namespace TestingContextKeys { type: 'boolean', description: localize('testing.testResultOutdated', 'Value available in editor/content and testing/message/context when the result is outdated') }); + export const testResultState = new RawContextKey('testResultState', undefined, { + type: 'string', + description: localize('testing.testResultState', 'Value available testing/item/result indicating the state of the item.') + }); } diff --git a/code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts similarity index 100% rename from code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts rename to code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts diff --git a/code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts similarity index 100% rename from code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts rename to code/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts diff --git a/code/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/code/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index 569661d627d..e0923164c97 100644 --- a/code/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/code/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -5,14 +5,14 @@ import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; import { TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; -import { ITreeSorter } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; type SerializedTree = { e: string; children?: SerializedTree[]; data?: string }; @@ -31,14 +31,22 @@ class TestObjectTree extends ObjectTree { }, [ { - disposeTemplate: () => undefined, - renderElement: (node, _index, container: HTMLElement) => { - Object.assign(container.dataset, node.element); - container.textContent = `${node.depth}:${serializer(node.element)}`; + disposeTemplate: ({ store }) => store.dispose(), + renderElement: ({ depth, element }, _index, { container, store }) => { + const render = () => { + container.textContent = `${depth}:${serializer(element)}`; + Object.assign(container.dataset, element); + }; + render(); + + if (element instanceof TestItemTreeElement) { + store.add(element.onChange(render)); + } }, - renderTemplate: c => c, + disposeElement: (_el, _index, { store }) => store.clear(), + renderTemplate: container => ({ container, store: new DisposableStore() }), templateId: 'default' - } + } as ITreeRenderer ], { sorter: sorter ?? { diff --git a/code/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/code/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 06476f564c8..87503bb990c 100644 --- a/code/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/code/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -49,13 +49,13 @@ import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { isString } from 'vs/base/common/types'; import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; import { ILocalizedString } from 'vs/platform/action/common/action'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const ItemHeight = 22; diff --git a/code/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/code/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index b5e2de0ed52..9f6cc07ba5f 100644 --- a/code/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/code/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -38,7 +38,6 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService export class ReleaseNotesManager { private readonly _simpleSettingRenderer: SimpleSettingRenderer; private readonly _releaseNotesCache = new Map>(); - private scrollPosition: { x: number; y: number } | undefined; private _currentReleaseNotes: WebviewInput | undefined = undefined; private _lastText: string | undefined; @@ -72,11 +71,9 @@ export class ReleaseNotesManager { if (!this._currentReleaseNotes || !this._lastText) { return; } - const captureScroll = this.scrollPosition; const html = await this.renderBody(this._lastText); if (this._currentReleaseNotes) { this._currentReleaseNotes.webview.setHtml(html); - this._currentReleaseNotes.webview.postMessage({ type: 'setScroll', value: { scrollPosition: captureScroll } }); } } @@ -116,8 +113,6 @@ export class ReleaseNotesManager { disposables.add(this._currentReleaseNotes.webview.onMessage(e => { if (e.message.type === 'showReleaseNotes') { this._configurationService.updateValue('update.showReleaseNotes', e.message.value); - } else if (e.message.type === 'scroll') { - this.scrollPosition = e.message.value.scrollPosition; } else if (e.message.type === 'clickSetting') { const x = this._currentReleaseNotes?.webview.container.offsetLeft + e.message.value.x; const y = this._currentReleaseNotes?.webview.container.offsetTop + e.message.value.y; @@ -234,7 +229,7 @@ export class ReleaseNotesManager { } private async onDidClickLink(uri: URI) { - if (uri.scheme === Schemas.codeSetting || uri.scheme === Schemas.codeFeature) { + if (uri.scheme === Schemas.codeSetting) { // handled in receive message } else { this.addGAParameters(uri, 'ReleaseNotes') @@ -353,64 +348,6 @@ export class ReleaseNotesManager { margin-right: 8px; } - /* codefeature */ - - .codefeature-container { - display: flex; - } - - .codefeature { - position: relative; - display: inline-block; - width: 46px; - height: 24px; - } - - .codefeature-container input { - display: none; - } - - .toggle { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--vscode-button-background); - transition: .4s; - border-radius: 24px; - } - - .toggle:before { - position: absolute; - content: ""; - height: 16px; - width: 16px; - left: 4px; - bottom: 4px; - background-color: var(--vscode-editor-foreground); - transition: .4s; - border-radius: 50%; - } - - input:checked+.codefeature > .toggle:before { - transform: translateX(22px); - } - - .codefeature-container:has(input) .title { - line-height: 30px; - padding-left: 4px; - font-weight: bold; - } - - .codefeature-container:has(input:checked) .title:after { - content: "${nls.localize('disableFeature', "Disable this feature")}"; - } - .codefeature-container:has(input:not(:checked)) .title:after { - content: "${nls.localize('enableFeature', "Enable this feature")}"; - } - header { display: flex; align-items: center; padding-top: 1em; } @@ -443,40 +380,13 @@ export class ReleaseNotesManager { window.addEventListener('message', event => { if (event.data.type === 'showReleaseNotes') { input.checked = event.data.value; - } else if (event.data.type === 'setScroll') { - window.scrollTo(event.data.value.scrollPosition.x, event.data.value.scrollPosition.y); - } else if (event.data.type === 'setFeaturedSettings') { - for (const [settingId, value] of event.data.value) { - const setting = document.getElementById(settingId); - if (setting instanceof HTMLInputElement) { - setting.checked = value; - } - } } }); - window.onscroll = () => { - vscode.postMessage({ - type: 'scroll', - value: { - scrollPosition: { - x: window.scrollX, - y: window.scrollY - } - } - }); - }; - window.addEventListener('click', event => { const href = event.target.href ?? event.target.parentElement.href ?? event.target.parentElement.parentElement?.href; - if (href && (href.startsWith('${Schemas.codeSetting}') || href.startsWith('${Schemas.codeFeature}'))) { + if (href && (href.startsWith('${Schemas.codeSetting}'))) { vscode.postMessage({ type: 'clickSetting', value: { uri: href, x: event.clientX, y: event.clientY }}); - if (href.startsWith('${Schemas.codeFeature}')) { - const featureInput = event.target.parentElement.previousSibling; - if (featureInput instanceof HTMLInputElement) { - featureInput.checked = !featureInput.checked; - } - } } }); @@ -506,7 +416,6 @@ export class ReleaseNotesManager { private onDidChangeActiveWebviewEditor(input: WebviewInput | undefined): void { if (input && input === this._currentReleaseNotes) { this.updateCheckboxWebview(); - this.updateFeaturedSettingsWebview(); } } @@ -518,13 +427,4 @@ export class ReleaseNotesManager { }); } } - - private updateFeaturedSettingsWebview() { - if (this._currentReleaseNotes) { - this._currentReleaseNotes.webview.postMessage({ - type: 'setFeaturedSettings', - value: this._simpleSettingRenderer.featuredSettingStates - }); - } - } } diff --git a/code/src/vs/workbench/contrib/update/browser/update.ts b/code/src/vs/workbench/contrib/update/browser/update.ts index 969c563d5de..af1b3d507c2 100644 --- a/code/src/vs/workbench/contrib/update/browser/update.ts +++ b/code/src/vs/workbench/contrib/update/browser/update.ts @@ -17,7 +17,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { ReleaseNotesManager } from 'vs/workbench/contrib/update/browser/releaseNotesEditor'; -import { isWeb, isWindows } from 'vs/base/common/platform'; +import { isMacintosh, isWeb, isWindows } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; @@ -173,6 +173,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu @IContextKeyService private readonly contextKeyService: IContextKeyService, @IProductService private readonly productService: IProductService, @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IHostService private readonly hostService: IHostService ) { super(); @@ -241,10 +242,13 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu break; case StateType.Ready: { - const currentVersion = parseVersion(this.productService.version); - const nextVersion = parseVersion(state.update.productVersion); - this.majorMinorUpdateAvailableContextKey.set(Boolean(currentVersion && nextVersion && isMajorMinorUpdate(currentVersion, nextVersion))); - this.onUpdateReady(state.update); + const productVersion = state.update.productVersion; + if (productVersion) { + const currentVersion = parseVersion(this.productService.version); + const nextVersion = parseVersion(productVersion); + this.majorMinorUpdateAvailableContextKey.set(Boolean(currentVersion && nextVersion && isMajorMinorUpdate(currentVersion, nextVersion))); + this.onUpdateReady(state.update); + } break; } } @@ -298,6 +302,11 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu return; } + const productVersion = update.productVersion; + if (!productVersion) { + return; + } + this.notificationService.prompt( severity.Info, nls.localize('thereIsUpdateAvailable', "There is an available update."), @@ -310,21 +319,33 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu }, { label: nls.localize('releaseNotes', "Release Notes"), run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, update.productVersion)); + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); } }] ); } - // windows fast updates (target === system) + // windows fast updates private onUpdateDownloaded(update: IUpdate): void { + if (isMacintosh) { + return; + } + if (this.configurationService.getValue('update.enableWindowsBackgroundUpdates') && this.productService.target === 'user') { + return; + } + if (!this.shouldShowNotification()) { return; } + const productVersion = update.productVersion; + if (!productVersion) { + return; + } + this.notificationService.prompt( severity.Info, - nls.localize('updateAvailable', "There's an update available: {0} {1}", this.productService.nameLong, update.productVersion), + nls.localize('updateAvailable', "There's an update available: {0} {1}", this.productService.nameLong, productVersion), [{ label: nls.localize('installUpdate', "Install Update"), run: () => this.updateService.applyUpdate() @@ -334,7 +355,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu }, { label: nls.localize('releaseNotes', "Release Notes"), run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, update.productVersion)); + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); } }] ); @@ -354,12 +375,12 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu run: () => { } }]; - // TODO@joao check why snap updates send `update` as falsy - if (update.productVersion) { + const productVersion = update.productVersion; + if (productVersion) { actions.push({ label: nls.localize('releaseNotes', "Release Notes"), run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, update.productVersion)); + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); } }); } @@ -460,8 +481,11 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu return; } - const version = this.updateService.state.update.version; - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, version)); + const productVersion = this.updateService.state.update.productVersion; + if (productVersion) { + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } + }); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '7_update', @@ -509,7 +533,7 @@ export class SwitchProductQualityContribution extends Disposable implements IWor const newQuality = quality === 'stable' ? 'insider' : 'stable'; const commandId = `update.switchQuality.${newQuality}`; const isSwitchingToInsiders = newQuality === 'insider'; - registerAction2(class SwitchQuality extends Action2 { + this._register(registerAction2(class SwitchQuality extends Action2 { constructor() { super({ id: commandId, @@ -604,7 +628,7 @@ export class SwitchProductQualityContribution extends Disposable implements IWor }); return result; } - }); + })); } } } diff --git a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index e86a7c07e96..e213af65498 100644 --- a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -425,7 +425,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } private registerDeleteProfileAction(): void { - registerAction2(class DeleteProfileAction extends Action2 { + this._register(registerAction2(class DeleteProfileAction extends Action2 { constructor() { super({ id: 'workbench.profiles.actions.deleteProfile', @@ -473,7 +473,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } } } - }); + })); } private registerHelpAction(): void { diff --git a/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 0787368edba..08c30bd1f30 100644 --- a/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/code/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -918,7 +918,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: syncNowCommand.id, label: `${SYNC_TITLE.value}: ${syncNowCommand.title.original}`, description: syncNowCommand.description(that.userDataSyncService) }); if (that.userDataSyncEnablementService.canToggleEnablement()) { const account = that.userDataSyncWorkbenchService.current; - items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getLabel(account.authenticationProviderId)})` : undefined }); + items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getProvider(account.authenticationProviderId).label})` : undefined }); } quickPick.items = items; disposables.add(quickPick.onDidAccept(() => { diff --git a/code/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/code/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 248d8c0756f..e81df189fa1 100644 --- a/code/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/code/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -97,7 +97,7 @@ export class UserDataSyncDataViews extends Disposable { order: 300, }], container); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.sync.editMachineName`, @@ -116,9 +116,9 @@ export class UserDataSyncDataViews extends Disposable { await treeView.refresh(); } } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.sync.turnOffSyncOnMachine`, @@ -134,7 +134,7 @@ export class UserDataSyncDataViews extends Disposable { await treeView.refresh(); } } - }); + })); } @@ -221,7 +221,7 @@ export class UserDataSyncDataViews extends Disposable { } private registerDataViewActions(viewId: string) { - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.sync.${viewId}.resolveResource`, @@ -237,9 +237,9 @@ export class UserDataSyncDataViews extends Disposable { const editorService = accessor.get(IEditorService); await editorService.openEditor({ resource: URI.parse(resource), options: { pinned: true } }); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.sync.${viewId}.compareWithLocal`, @@ -262,9 +262,9 @@ export class UserDataSyncDataViews extends Disposable { undefined ); } - }); + })); - registerAction2(class extends Action2 { + this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.sync.${viewId}.replaceCurrent`, @@ -290,7 +290,7 @@ export class UserDataSyncDataViews extends Disposable { return userDataSyncService.replace({ created: syncResourceHandle.created, uri: URI.revive(syncResourceHandle.uri) }); } } - }); + })); } diff --git a/code/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/code/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index 0c60dbd1e68..6b6be89f393 100644 --- a/code/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/code/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Dimension } from 'vs/base/browser/dom'; +import { Dimension, getWindowById } from 'vs/base/browser/dom'; import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { CodeWindow } from 'vs/base/browser/window'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IOverlayWebview, IWebview, IWebviewElement, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_ENABLED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, WebviewContentOptions, WebviewExtensionDescription, WebviewInitInfo, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; /** @@ -36,6 +37,9 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private _owner: any = undefined; + private _windowId: number | undefined = undefined; + private get window() { return getWindowById(this._windowId, true).window; } + private readonly _scopedContextKeyService = this._register(new MutableDisposable()); private _findWidgetVisible: IContextKey | undefined; private _findWidgetEnabled: IContextKey | undefined; @@ -49,7 +53,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { public constructor( initInfo: WebviewInitInfo, - @ILayoutService private readonly _layoutService: ILayoutService, + @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, @IWebviewService private readonly _webviewService: IWebviewService, @IContextKeyService private readonly _baseContextKeyService: IContextKeyService ) { @@ -103,21 +107,32 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { // Webviews cannot be reparented in the dom as it will destroy their contents. // Mount them to a high level node to avoid this. - this._layoutService.mainContainer.appendChild(node); + this._layoutService.getContainer(this.window).appendChild(node); } return this._container.domNode; } - public claim(owner: any, scopedContextKeyService: IContextKeyService | undefined) { + public claim(owner: any, targetWindow: CodeWindow, scopedContextKeyService: IContextKeyService | undefined) { if (this._isDisposed) { return; } const oldOwner = this._owner; + if (this._windowId !== targetWindow.vscodeWindowId) { + // moving to a new window + this.release(oldOwner); + // since we are moving to a new window, we need to dispose the webview and recreate + this._webview.clear(); + this._webviewEvents.clear(); + this._container?.domNode.remove(); + this._container = undefined; + } + this._owner = owner; - this._show(); + this._windowId = targetWindow.vscodeWindowId; + this._show(targetWindow); if (oldOwner !== owner) { const contextKeyService = (scopedContextKeyService || this._baseContextKeyService); @@ -168,6 +183,22 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { return; } + const whenContainerStylesLoaded = this._layoutService.whenContainerStylesLoaded(this.window); + if (whenContainerStylesLoaded) { + // In floating windows, we need to ensure that the + // container is ready for us to compute certain + // layout related properties. + whenContainerStylesLoaded.then(() => this.doLayoutWebviewOverElement(element, dimension, clippingContainer)); + } else { + this.doLayoutWebviewOverElement(element, dimension, clippingContainer); + } + } + + private doLayoutWebviewOverElement(element: HTMLElement, dimension?: Dimension, clippingContainer?: HTMLElement) { + if (!this._container || !this._container.domNode.parentElement) { + return; + } + const frameRect = element.getBoundingClientRect(); const containerRect = this._container.domNode.parentElement.getBoundingClientRect(); const parentBorderTop = (containerRect.height - this._container.domNode.parentElement.clientHeight) / 2.0; @@ -184,7 +215,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { } } - private _show() { + private _show(targetWindow: CodeWindow) { if (this._isDisposed) { throw new Error('OverlayWebview is disposed'); } @@ -215,7 +246,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { this._findWidgetEnabled?.set(!!this.options.enableFindWidget); - webview.mountTo(this.container); + webview.mountTo(this.container, targetWindow); // Forward events from inner webview to outer listeners this._webviewEvents.clear(); diff --git a/code/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/code/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index fe3fe2dfef2..7efdc8a0e65 100644 --- a/code/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/code/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -46,7 +46,8 @@ const interval = 250; let isFocused = document.hasFocus(); setInterval(() => { - const isCurrentlyFocused = document.hasFocus(); + const target = getActiveFrame(); + const isCurrentlyFocused = document.hasFocus() || !!(target && target.contentDocument && target.contentDocument.body.classList.contains('vscode-context-menu-visible')); if (isCurrentlyFocused === isFocused) { return; } @@ -128,6 +129,10 @@ border-radius: 4px; } + pre code { + padding: 0; + } + blockquote { background: var(--vscode-textBlockQuote-background); border-color: var(--vscode-textBlockQuote-border); diff --git a/code/src/vs/workbench/contrib/webview/browser/pre/index.html b/code/src/vs/workbench/contrib/webview/browser/pre/index.html index 47035032604..54d741062ac 100644 --- a/code/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/code/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -47,7 +47,8 @@ const interval = 250; let isFocused = document.hasFocus(); setInterval(() => { - const isCurrentlyFocused = document.hasFocus(); + const target = getActiveFrame(); + const isCurrentlyFocused = document.hasFocus() || !!(target && target.contentDocument && target.contentDocument.body.classList.contains('vscode-context-menu-visible')); if (isCurrentlyFocused === isFocused) { return; } @@ -129,6 +130,10 @@ border-radius: 4px; } + pre code { + padding: 0; + } + blockquote { background: var(--vscode-textBlockQuote-background); border-color: var(--vscode-textBlockQuote-border); diff --git a/code/src/vs/workbench/contrib/webview/browser/themeing.ts b/code/src/vs/workbench/contrib/webview/browser/themeing.ts index 4fd074d18ae..b63bfffca2d 100644 --- a/code/src/vs/workbench/contrib/webview/browser/themeing.ts +++ b/code/src/vs/workbench/contrib/webview/browser/themeing.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { IWorkbenchThemeService, IWorkbenchColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; +import { IWorkbenchColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { WebviewStyles } from 'vs/workbench/contrib/webview/browser/webview'; interface WebviewThemeData { diff --git a/code/src/vs/workbench/contrib/webview/browser/webview.ts b/code/src/vs/workbench/contrib/webview/browser/webview.ts index dfd7c6fc5b4..14e925969a1 100644 --- a/code/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/code/src/vs/workbench/contrib/webview/browser/webview.ts @@ -78,7 +78,6 @@ export interface WebviewInitInfo { readonly contentOptions: WebviewContentOptions; readonly extension: WebviewExtensionDescription | undefined; - readonly codeWindow?: CodeWindow; } export const enum WebviewContentPurpose { @@ -278,7 +277,7 @@ export interface IWebviewElement extends IWebview { * * @param parent Element to append the webview to. */ - mountTo(parent: HTMLElement): void; + mountTo(parent: HTMLElement, targetWindow: CodeWindow): void; } /** @@ -308,7 +307,7 @@ export interface IOverlayWebview extends IWebview { * @param claimant Identifier for the object claiming the webview. * This must match the `claimant` passed to {@link IOverlayWebview.release}. */ - claim(claimant: any, scopedContextKeyService: IContextKeyService | undefined): void; + claim(claimant: any, targetWindow: CodeWindow, scopedContextKeyService: IContextKeyService | undefined): void; /** * Release ownership of the webview. diff --git a/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 6514979da3d..0d950ccf6fa 100644 --- a/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener, EventType, getActiveWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, getWindowById } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { promiseWithResolvers, ThrottledDelayer } from 'vs/base/common/async'; import { streamToBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -37,7 +37,7 @@ import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/web import { FromWebviewMessage, KeyEvent, ToWebviewMessage } from 'vs/workbench/contrib/webview/browser/webviewMessages'; import { decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/contrib/webview/common/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { $window } from 'vs/base/browser/window'; +import { CodeWindow } from 'vs/base/browser/window'; interface WebviewContent { readonly html: string; @@ -88,7 +88,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD */ public readonly origin: string; - private readonly _encodedWebviewOriginPromise: Promise; + private _windowId: number | undefined = undefined; + private get window() { return typeof this._windowId === 'number' ? getWindowById(this._windowId)?.window : undefined; } + + private _encodedWebviewOriginPromise?: Promise; private _encodedWebviewOrigin: string | undefined; protected get platform(): string { return 'browser'; } @@ -103,7 +106,12 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD if (!this._focused) { return false; } - if ($window.document.activeElement && $window.document.activeElement !== this.element) { + // code window is only available after the webview is mounted. + if (!this.window) { + return false; + } + + if (this.window.document.activeElement && this.window.document.activeElement !== this.element) { // looks like https://github.com/microsoft/vscode/issues/132641 // where the focus is actually not in the `