diff --git a/.eslintrc.json b/.eslintrc.json index b0f3da995bb..27860beea93 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -163,8 +163,6 @@ "src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts", "src/vs/editor/test/common/services/languageService.test.ts", "src/vs/editor/test/node/classification/typescript.test.ts", - "src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts", - "src/vs/editor/test/node/diffing/fixtures.test.ts", "src/vs/platform/configuration/test/common/configuration.test.ts", "src/vs/platform/extensions/test/common/extensionValidator.test.ts", "src/vs/platform/opener/test/common/opener.test.ts", diff --git a/.github/workflows/build-release-macos.yml b/.github/workflows/build-release-macos.yml index 0a11a8224ec..a101aab0bc6 100644 --- a/.github/workflows/build-release-macos.yml +++ b/.github/workflows/build-release-macos.yml @@ -53,7 +53,11 @@ jobs: run: | # Connect to the host; if there is a screen session running, do # nothing, but if there isn't one, start one up now. - ssh -o "StrictHostKeyChecking no" user229818@NY503.macincloud.com "/bin/zsh -li -c \"if screen -list | grep -q 'No Sockets found'; then screen -dmS agent_session /bin/zsh -li -c 'cd ./actions-runner && ./run.sh'; fi\"" + # + # Lower the scheduling priority of the agent to reduce the odds of it + # consuming enough resources to trigger a reboot of the MacInCloud + # host. + ssh -o "StrictHostKeyChecking no" user229818@NY503.macincloud.com "/bin/zsh -li -c \"if screen -list | grep -q 'No Sockets found'; then screen -dmS agent_session /bin/zsh -li -c 'cd ./actions-runner && nice -n 19 ./run.sh'; fi\"" build-archs: name: Build macOS diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index 576bfa12fc3..71954a68c10 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -1,4 +1,9 @@ name: "Deep Classifier: Runner" + +permissions: + id-token: write + contents: read + on: schedule: - cron: 0 * * * * @@ -9,7 +14,13 @@ on: jobs: main: runs-on: ubuntu-latest + environment: main steps: + - uses: azure/login@v1 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + allow-no-subscriptions: true - name: Checkout Actions uses: actions/checkout@v4 with: @@ -47,8 +58,4 @@ jobs: with: configPath: classifier allowLabels: "info-needed|new release|error-telemetry|*english-please|translation-required" - tenantId: ${{secrets.TOOLS_TENANT_ID}} - clientId: ${{secrets.TOOLS_CLIENT_ID}} - clientSecret: ${{secrets.TOOLS_CLIENT_SECRET}} - clientScope: ${{secrets.TOOLS_CLIENT_SCOPE}} token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.gitignore b/.gitignore index 77b3927bf87..232848ee7d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ npm-debug.log Thumbs.db node_modules/ .build/ +.vscode/extensions/**/out/ extensions/**/dist/ /out*/ /extensions/**/out/ diff --git a/.nvmrc b/.nvmrc index 4a1f488b6c3..a9d087399d7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.17.1 +18.19.0 diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 2a6f3ec1bc5..6b8a385ec42 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.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/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index ee1084be56d..750e53e4b26 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.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/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index a286082c738..ab59f23283f 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/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/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 2a3f9159703..b23dacf87e4 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.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/.vscode/settings.json b/.vscode/settings.json index 0f62a0752e2..4689dbc46ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -173,7 +173,6 @@ }, "css.format.spaceAroundSelectorSeparator": true, "inlineChat.mode": "live", - "testing.defaultGutterClickAction": "contextMenu", "editor.rulers": [ 100 ], diff --git a/.yarnrc b/.yarnrc index 19c5cb1eb8f..616968bddff 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "27.3.2" -ms_build_id "26836302" +target "28.2.8" +ms_build_id "27744544" runtime "electron" build_from_source "true" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index ddd05229f29..efbc726a2cd 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -169,7 +169,7 @@ OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -atom/language-sass 0.62.1 - MIT +atom/language-sass 0.61.4 - MIT https://github.com/atom/language-sass The MIT License (MIT) @@ -517,7 +517,7 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -go-syntax 0.5.1 - MIT +go-syntax 0.6.1 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -777,7 +777,7 @@ SOFTWARE. --------------------------------------------------------- -jeff-hykin/better-shell-syntax 1.6.2 - MIT +jeff-hykin/better-shell-syntax 1.7.1 - MIT https://github.com/jeff-hykin/better-shell-syntax MIT License @@ -833,7 +833,7 @@ SOFTWARE. --------------------------------------------------------- -jlelong/vscode-latex-basics 1.5.4 - MIT +jlelong/vscode-latex-basics 1.6.0 - MIT https://github.com/jlelong/vscode-latex-basics Copyright (c) vscode-latex-basics authors @@ -2490,7 +2490,7 @@ Creative Commons may be contacted at creativecommons.org. --------------------------------------------------------- -vscode-logfile-highlighter 2.15.0 - MIT +vscode-logfile-highlighter 2.17.0 - MIT https://github.com/emilast/vscode-logfile-highlighter The MIT License (MIT) diff --git a/build/.cachesalt b/build/.cachesalt index 89591977f28..8051d84124e 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-02-05T09:34:15.476Z +2024-03-18T08:47:22.277Z diff --git a/build/azure-pipelines/alpine/cli-build-alpine.yml b/build/azure-pipelines/alpine/cli-build-alpine.yml index 5d4e79424d2..a6442dfe290 100644 --- a/build/azure-pipelines/alpine/cli-build-alpine.yml +++ b/build/azure-pipelines/alpine/cli-build-alpine.yml @@ -33,7 +33,7 @@ steps: workingDirectory: build displayName: Install pipeline build - - template: ../cli/cli-apply-patches.yml + - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 displayName: Download openssl prebuilt @@ -58,7 +58,7 @@ steps: sudo ln -s "/usr/bin/g++" "/usr/bin/musl-g++" || echo "link exists" displayName: Install musl build dependencies - - template: ../cli/install-rust-posix.yml + - template: ../cli/install-rust-posix.yml@self parameters: targets: - ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}: @@ -67,7 +67,7 @@ steps: - x86_64-unknown-linux-musl - ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_CLI_TARGET: aarch64-unknown-linux-musl VSCODE_CLI_ARTIFACT: vscode_cli_alpine_arm64_cli @@ -80,7 +80,7 @@ steps: OPENSSL_STATIC: "1" - ${{ if eq(parameters.VSCODE_BUILD_ALPINE, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_CLI_TARGET: x86_64-unknown-linux-musl VSCODE_CLI_ARTIFACT: vscode_cli_alpine_x64_cli @@ -92,14 +92,23 @@ steps: OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-linux-musl/include OPENSSL_STATIC: "1" - - ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: vscode_cli_alpine_arm64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + - ${{ if not(parameters.VSCODE_CHECK_ONLY) }}: + - ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/vscode_cli_alpine_arm64_cli.tar.gz + artifactName: vscode_cli_alpine_arm64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code Alpine arm64 CLI" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish vscode_cli_alpine_arm64_cli artifact - - ${{ if eq(parameters.VSCODE_BUILD_ALPINE, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: vscode_cli_alpine_x64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + - ${{ if eq(parameters.VSCODE_BUILD_ALPINE, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/vscode_cli_alpine_x64_cli.tar.gz + artifactName: vscode_cli_alpine_x64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code Alpine x64 CLI" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish vscode_cli_alpine_x64_cli artifact diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index 23592e2dfa5..b1308555e10 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -13,7 +13,7 @@ steps: versionFilePath: .nvmrc nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - template: ../distro/download-distro.yml + - template: ../distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -118,7 +118,7 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality - - template: ../common/install-builtin-extensions.yml + - template: ../common/install-builtin-extensions.yml@self - script: | set -e @@ -126,8 +126,10 @@ steps: yarn gulp vscode-reh-$TARGET-min-ci (cd .. && mv vscode-reh-$TARGET vscode-server-$TARGET) # TODO@joaomoreno ARCHIVE_PATH=".build/linux/server/vscode-server-$TARGET.tar.gz" + DIR_PATH="$(realpath ../vscode-server-$TARGET)" mkdir -p $(dirname $ARCHIVE_PATH) tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-$TARGET + echo "##vso[task.setvariable variable=SERVER_DIR_PATH]$DIR_PATH" echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -139,8 +141,10 @@ steps: yarn gulp vscode-reh-web-$TARGET-min-ci (cd .. && mv vscode-reh-web-$TARGET vscode-server-$TARGET-web) # TODO@joaomoreno ARCHIVE_PATH=".build/linux/web/vscode-server-$TARGET-web.tar.gz" + DIR_PATH="$(realpath ../vscode-server-$TARGET-web)" mkdir -p $(dirname $ARCHIVE_PATH) tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-$TARGET-web + echo "##vso[task.setvariable variable=WEB_DIR_PATH]$DIR_PATH" echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -150,36 +154,40 @@ steps: condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) displayName: Generate artifact prefix - - script: mkdir $(agent.builddirectory)/vscode-alpine-$(VSCODE_ARCH) - displayName: Make folder for SBOM - - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM + - task: 1ES.PublishPipelineArtifact@1 inputs: - BuildDropPath: $(agent.builddirectory)/vscode-alpine-$(VSCODE_ARCH) - PackageName: Visual Studio Code Server - - - publish: $(agent.builddirectory)/vscode-alpine-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM - artifact: $(ARTIFACT_PREFIX)sbom_vscode_alpine_$(VSCODE_ARCH) - - - publish: $(SERVER_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_server_alpine_$(VSCODE_ARCH)_archive-unsigned + targetPath: $(SERVER_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_server_alpine_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(SERVER_DIR_PATH) + sbomPackageName: "VS Code Alpine $(VSCODE_ARCH) Server" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish server archive condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], ''), ne(variables['VSCODE_ARCH'], 'x64')) - - publish: $(WEB_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_web_alpine_$(VSCODE_ARCH)_archive-unsigned + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_alpine_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(WEB_DIR_PATH) + sbomPackageName: "VS Code Alpine $(VSCODE_ARCH) Web" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish web server archive condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], ''), ne(variables['VSCODE_ARCH'], 'x64')) - # Legacy x64 artifact name - - publish: $(SERVER_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_server_linux_alpine_archive-unsigned + # same as above, keep legacy name + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SERVER_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_server_linux_alpine_archive-unsigned + sbomEnabled: false displayName: Publish x64 server archive condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], ''), eq(variables['VSCODE_ARCH'], 'x64')) - - publish: $(WEB_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_web_linux_alpine_archive-unsigned + # same as above, keep legacy name + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_linux_alpine_archive-unsigned + sbomEnabled: false displayName: Publish x64 web server archive condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], ''), eq(variables['VSCODE_ARCH'], 'x64')) diff --git a/build/azure-pipelines/cli/cli-apply-patches.yml b/build/azure-pipelines/cli/cli-apply-patches.yml index b96aa4ef7dd..2815124efb6 100644 --- a/build/azure-pipelines/cli/cli-apply-patches.yml +++ b/build/azure-pipelines/cli/cli-apply-patches.yml @@ -1,5 +1,5 @@ steps: - - template: ../distro/download-distro.yml + - template: ../distro/download-distro.yml@self - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 8d8b313253e..267682f7f6d 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -110,13 +110,14 @@ steps: Write-Host "##vso[task.setvariable variable=VSCODE_CLI_APPLICATION_NAME]$env:VSCODE_CLI_APPLICATION_NAME" - Move-Item -Path $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code.exe -Destination "$(Build.ArtifactStagingDirectory)/${env:VSCODE_CLI_APPLICATION_NAME}.exe" + New-Item -ItemType Directory -Force -Path "$(Build.ArtifactStagingDirectory)/cli" + Move-Item -Path $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code.exe -Destination "$(Build.ArtifactStagingDirectory)/cli/${env:VSCODE_CLI_APPLICATION_NAME}.exe" displayName: Stage CLI - task: ArchiveFiles@2 displayName: Archive CLI inputs: - rootFolderOrFile: $(Build.ArtifactStagingDirectory)/$(VSCODE_CLI_APPLICATION_NAME).exe + rootFolderOrFile: $(Build.ArtifactStagingDirectory)/cli/$(VSCODE_CLI_APPLICATION_NAME).exe includeRootFolder: false archiveType: zip archiveFile: $(Build.ArtifactStagingDirectory)/${{ parameters.VSCODE_CLI_ARTIFACT }}.zip @@ -127,43 +128,19 @@ steps: VSCODE_CLI_APPLICATION_NAME=$(node -p "require(\"$VSCODE_CLI_PRODUCT_JSON\").applicationName") echo "##vso[task.setvariable variable=VSCODE_CLI_APPLICATION_NAME]$VSCODE_CLI_APPLICATION_NAME" - mv $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code $(Build.ArtifactStagingDirectory)/$VSCODE_CLI_APPLICATION_NAME + mkdir -p $(Build.ArtifactStagingDirectory)/cli + mv $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code $(Build.ArtifactStagingDirectory)/cli/$VSCODE_CLI_APPLICATION_NAME displayName: Stage CLI - - ${{ if contains(parameters.VSCODE_CLI_TARGET, '-darwin') }}: - - task: ArchiveFiles@2 - displayName: Archive CLI - inputs: - rootFolderOrFile: $(Build.ArtifactStagingDirectory)/$(VSCODE_CLI_APPLICATION_NAME) - includeRootFolder: false + - task: ArchiveFiles@2 + displayName: Archive CLI + inputs: + rootFolderOrFile: $(Build.ArtifactStagingDirectory)/cli/$(VSCODE_CLI_APPLICATION_NAME) + includeRootFolder: false + ${{ if contains(parameters.VSCODE_CLI_TARGET, '-darwin') }}: archiveType: zip archiveFile: $(Build.ArtifactStagingDirectory)/${{ parameters.VSCODE_CLI_ARTIFACT }}.zip - - - ${{ else }}: - - task: ArchiveFiles@2 - displayName: Archive CLI - inputs: - rootFolderOrFile: $(Build.ArtifactStagingDirectory)/$(VSCODE_CLI_APPLICATION_NAME) - includeRootFolder: false + ${{ else }}: archiveType: tar tarCompression: gz archiveFile: $(Build.ArtifactStagingDirectory)/${{ parameters.VSCODE_CLI_ARTIFACT }}.tar.gz - - # Make a folder for the SBOM for the specific artifact - - ${{ if contains(parameters.VSCODE_CLI_TARGET, '-windows-') }}: - - powershell: mkdir $(Build.ArtifactStagingDirectory)/sbom_${{ parameters.VSCODE_CLI_ARTIFACT }} - displayName: Make folder for SBOM (Windows) - - - ${{ else }}: - - script: mkdir $(Build.ArtifactStagingDirectory)/sbom_${{ parameters.VSCODE_CLI_ARTIFACT }} - displayName: Make folder for SBOM (non-Windows) - - # The if cases above are for different OSes, - # but we're still in the branch where the cli is being published in general. - # Generate and publish an SBOM. - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM - inputs: - BuildComponentPath: $(Build.SourcesDirectory)/cli - BuildDropPath: $(Build.ArtifactStagingDirectory)/sbom_${{ parameters.VSCODE_CLI_ARTIFACT }} - PackageName: Visual Studio Code CLI diff --git a/build/azure-pipelines/cli/cli-darwin-sign.yml b/build/azure-pipelines/cli/cli-darwin-sign.yml index 925d8435dae..75a8235ff3a 100644 --- a/build/azure-pipelines/cli/cli-darwin-sign.yml +++ b/build/azure-pipelines/cli/cli-darwin-sign.yml @@ -26,6 +26,12 @@ steps: artifact: ${{ target }} path: $(Build.ArtifactStagingDirectory)/pkg/${{ target }} + - task: ExtractFiles@1 + displayName: Extract artifact + inputs: + archiveFilePatterns: $(Build.ArtifactStagingDirectory)/pkg/${{ target }}/*.zip + destinationFolder: $(Build.ArtifactStagingDirectory)/sign/${{ target }} + - script: node build/azure-pipelines/common/sign $(Agent.ToolsDirectory)/esrpclient/*/*/net6.0/esrpcli.dll sign-darwin $(ESRP-PKI) $(esrp-aad-username) $(esrp-aad-password) $(Build.ArtifactStagingDirectory)/pkg "*.zip" displayName: Codesign @@ -40,6 +46,11 @@ steps: echo "##vso[task.setvariable variable=ASSET_ID]$ASSET_ID" displayName: Set asset id variable - - publish: $(Build.ArtifactStagingDirectory)/pkg/${{ target }}/$(ASSET_ID).zip + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/pkg/${{ target }}/$(ASSET_ID).zip + artifactName: $(ASSET_ID) + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/sign/${{ target }} + sbomPackageName: "VS Code macOS ${{ target }} CLI" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish signed artifact with ID $(ASSET_ID) - artifact: $(ASSET_ID) diff --git a/build/azure-pipelines/cli/cli-publish.yml b/build/azure-pipelines/cli/cli-publish.yml deleted file mode 100644 index fa3eacd0f96..00000000000 --- a/build/azure-pipelines/cli/cli-publish.yml +++ /dev/null @@ -1,28 +0,0 @@ -parameters: - - name: VSCODE_CLI_ARTIFACT - type: string - - name: VSCODE_CHECK_ONLY - type: boolean - default: false - -steps: - - ${{ if not(parameters.VSCODE_CHECK_ONLY) }}: - - ${{ if contains(parameters.VSCODE_CLI_ARTIFACT, 'win32') }}: - - publish: $(Build.ArtifactStagingDirectory)/${{ parameters.VSCODE_CLI_ARTIFACT }}.zip - artifact: ${{ parameters.VSCODE_CLI_ARTIFACT }} - displayName: Publish ${{ parameters.VSCODE_CLI_ARTIFACT }} artifact - - - ${{ else }}: - - ${{ if contains(parameters.VSCODE_CLI_ARTIFACT, 'darwin') }}: - - publish: $(Build.ArtifactStagingDirectory)/${{ parameters.VSCODE_CLI_ARTIFACT }}.zip - artifact: ${{ parameters.VSCODE_CLI_ARTIFACT }} - displayName: Publish ${{ parameters.VSCODE_CLI_ARTIFACT }} artifact - - - ${{ else }}: - - publish: $(Build.ArtifactStagingDirectory)/${{ parameters.VSCODE_CLI_ARTIFACT }}.tar.gz - artifact: ${{ parameters.VSCODE_CLI_ARTIFACT }} - displayName: Publish ${{ parameters.VSCODE_CLI_ARTIFACT }} artifact - - - publish: $(Build.ArtifactStagingDirectory)/sbom_${{ parameters.VSCODE_CLI_ARTIFACT }}/_manifest - displayName: Publish SBOM - artifact: sbom_${{ parameters.VSCODE_CLI_ARTIFACT }} diff --git a/build/azure-pipelines/cli/cli-win32-sign.yml b/build/azure-pipelines/cli/cli-win32-sign.yml index 10d305b92b3..f8d11e806f2 100644 --- a/build/azure-pipelines/cli/cli-win32-sign.yml +++ b/build/azure-pipelines/cli/cli-win32-sign.yml @@ -59,6 +59,11 @@ steps: archiveType: zip archiveFile: $(Build.ArtifactStagingDirectory)/$(ASSET_ID).zip - - publish: $(Build.ArtifactStagingDirectory)/$(ASSET_ID).zip + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/$(ASSET_ID).zip + artifactName: $(ASSET_ID) + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/sign/${{ target }} + sbomPackageName: "VS Code Windows ${{ target }} CLI" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish signed artifact with ID $(ASSET_ID) - artifact: $(ASSET_ID) diff --git a/build/azure-pipelines/cli/test.yml b/build/azure-pipelines/cli/test.yml index 29dcf502f6a..8b525845548 100644 --- a/build/azure-pipelines/cli/test.yml +++ b/build/azure-pipelines/cli/test.yml @@ -1,5 +1,5 @@ steps: - - template: ./install-rust-posix.yml + - template: ./install-rust-posix.yml@self - script: cargo clippy -- -D warnings workingDirectory: cli diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index e6b24921ac1..c990e3a7146 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -340,10 +340,11 @@ async function downloadArtifact(artifact, downloadPath) { } async function unzip(packagePath, outputPath) { return new Promise((resolve, reject) => { - yauzl.open(packagePath, { lazyEntries: true }, (err, zipfile) => { + yauzl.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { if (err) { return reject(err); } + const result = []; zipfile.on('entry', entry => { if (/\/$/.test(entry.fileName)) { zipfile.readEntry(); @@ -357,20 +358,21 @@ async function unzip(packagePath, outputPath) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); const ostream = fs.createWriteStream(filePath); ostream.on('finish', () => { - zipfile.close(); - resolve(filePath); + result.push(filePath); + zipfile.readEntry(); }); istream?.on('error', err => reject(err)); istream.pipe(ostream); }); } }); + zipfile.on('close', () => resolve(result)); zipfile.readEntry(); }); }); } // 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 +423,12 @@ function getPlatform(product, os, arch, type) { case 'client': return `linux-${arch}`; case 'server': - return `server-linux-${arch}`; + return isLegacy ? `server-linux-legacy-${arch}` : `server-linux-${arch}`; case 'web': - return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + if (arch === 'standalone') { + return 'web-standalone'; + } + return isLegacy ? `server-linux-legacy-${arch}-web` : `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -476,7 +481,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,14 +489,15 @@ 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); const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 const url = await releaseAndProvision(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT_SUBJECT_NAME'), e('RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME'), e('PROVISION_TENANT_ID'), e('PROVISION_AAD_USERNAME'), e('PROVISION_AAD_PASSWORD'), commit, quality, artifactFilePath); const asset = { platform, type, url, hash, sha256hash, size, supportsFastUpdate: true }; - log('Creating asset...', JSON.stringify(asset)); + log('Creating asset...', JSON.stringify(asset, undefined, 2)); await (0, retry_1.retry)(async (attempt) => { log(`Creating asset in Cosmos DB (attempt ${attempt})...`); const aadCredentials = new identity_1.ClientSecretCredential(e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_CLIENT_SECRET')); @@ -525,6 +531,9 @@ async function main() { if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') { stages.add('Linux'); } + if (e('VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER') === 'True') { + stages.add('LinuxLegacyServer'); + } if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') { stages.add('Alpine'); } @@ -568,12 +577,8 @@ async function main() { const downloadSpeedKBS = Math.round((archiveSize / 1024) / downloadDurationS); console.log(`[${artifact.name}] Successfully downloaded after ${Math.floor(downloadDurationS)} seconds(${downloadSpeedKBS} KB/s).`); }); - const artifactFilePath = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY')); - const artifactSize = fs.statSync(artifactFilePath).size; - if (artifactSize !== Number(artifact.resource.properties.artifactsize)) { - console.log(`[${artifact.name}] Artifact size mismatch.Expected ${artifact.resource.properties.artifactsize}. Actual ${artifactSize} `); - throw new Error(`Artifact size mismatch.`); - } + const artifactFilePaths = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY')); + const artifactFilePath = artifactFilePaths.filter(p => !/_manifest/.test(p))[0]; processing.add(artifact.name); const promise = new Promise((resolve, reject) => { const worker = new node_worker_threads_1.Worker(__filename, { workerData: { artifact, artifactFilePath } }); @@ -595,7 +600,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/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index f144a7be793..75065ffa2d3 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -483,13 +483,14 @@ async function downloadArtifact(artifact: Artifact, downloadPath: string): Promi } } -async function unzip(packagePath: string, outputPath: string): Promise { +async function unzip(packagePath: string, outputPath: string): Promise { return new Promise((resolve, reject) => { - yauzl.open(packagePath, { lazyEntries: true }, (err, zipfile) => { + yauzl.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { if (err) { return reject(err); } + const result: string[] = []; zipfile!.on('entry', entry => { if (/\/$/.test(entry.fileName)) { zipfile!.readEntry(); @@ -504,8 +505,8 @@ async function unzip(packagePath: string, outputPath: string): Promise { const ostream = fs.createWriteStream(filePath); ostream.on('finish', () => { - zipfile!.close(); - resolve(filePath); + result.push(filePath); + zipfile!.readEntry(); }); istream?.on('error', err => reject(err)); istream!.pipe(ostream); @@ -513,6 +514,7 @@ async function unzip(packagePath: string, outputPath: string): Promise { } }); + zipfile!.on('close', () => resolve(result)); zipfile!.readEntry(); }); }); @@ -531,7 +533,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 +584,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 ? `server-linux-legacy-${arch}` : `server-linux-${arch}`; case 'web': - return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; + if (arch === 'standalone') { + return 'web-standalone'; + } + return isLegacy ? `server-linux-legacy-${arch}-web` : `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -639,7 +644,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 +654,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); @@ -670,7 +676,7 @@ async function processArtifact(artifact: Artifact, artifactFilePath: string): Pr ); const asset: Asset = { platform, type, url, hash, sha256hash, size, supportsFastUpdate: true }; - log('Creating asset...', JSON.stringify(asset)); + log('Creating asset...', JSON.stringify(asset, undefined, 2)); await retry(async (attempt) => { log(`Creating asset in Cosmos DB (attempt ${attempt})...`); @@ -706,6 +712,7 @@ async function main() { const stages = new Set(['Compile', 'CompileCLI']); if (e('VSCODE_BUILD_STAGE_WINDOWS') === 'True') { stages.add('Windows'); } if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') { stages.add('Linux'); } + if (e('VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER') === 'True') { stages.add('LinuxLegacyServer'); } if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') { stages.add('Alpine'); } if (e('VSCODE_BUILD_STAGE_MACOS') === 'True') { stages.add('macOS'); } if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { stages.add('Web'); } @@ -748,13 +755,8 @@ async function main() { console.log(`[${artifact.name}] Successfully downloaded after ${Math.floor(downloadDurationS)} seconds(${downloadSpeedKBS} KB/s).`); }); - const artifactFilePath = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY')); - const artifactSize = fs.statSync(artifactFilePath).size; - - if (artifactSize !== Number(artifact.resource.properties.artifactsize)) { - console.log(`[${artifact.name}] Artifact size mismatch.Expected ${artifact.resource.properties.artifactsize}. Actual ${artifactSize} `); - throw new Error(`Artifact size mismatch.`); - } + const artifactFilePaths = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY')); + const artifactFilePath = artifactFilePaths.filter(p => !/_manifest/.test(p))[0]; processing.add(artifact.name); const promise = new Promise((resolve, reject) => { diff --git a/build/azure-pipelines/common/retry.js b/build/azure-pipelines/common/retry.js index 7b90b0cac5b..91f60bf24b2 100644 --- a/build/azure-pipelines/common/retry.js +++ b/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/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js index 4dba4765ff6..32996a7db03 100644 --- a/build/azure-pipelines/common/sign.js +++ b/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/build/azure-pipelines/config/CredScanSuppressions.json b/build/azure-pipelines/config/CredScanSuppressions.json index 312a5560cbd..bf52c06cf89 100644 --- a/build/azure-pipelines/config/CredScanSuppressions.json +++ b/build/azure-pipelines/config/CredScanSuppressions.json @@ -3,9 +3,87 @@ "suppressions": [ { "file": [ - "src/vs/base/test/common/uri.test.ts" + "src/vs/base/test/common/uri.test.ts", + "src/vs/workbench/api/test/browser/extHostTelemetry.test.ts" ], - "_justification": "These are not passwords, they are URIs." + "_justification": "These are dummy credentials in tests." + }, + { + "file": [ + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/stage/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/stage/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/prime/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/prime/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/parts/code/build/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/parts/code/install/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/parts/code/src/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/parts/code/build/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/parts/code/install/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/parts/code/src/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js" + ], + "_justification": "These are safe to ignore, since they are built artifacts (stable)." + }, + { + "file": [ + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-insiders-x64/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-insiders-x64/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-insiders-x64/stage/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-insiders-x64/stage/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-insiders-x64/prime/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-insiders-x64/prime/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-insiders-x64/parts/code/build/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-insiders-x64/parts/code/install/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-insiders-x64/parts/code/src/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-insiders-x64/parts/code/build/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-insiders-x64/parts/code/install/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-insiders-x64/parts/code/src/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js" + ], + "_justification": "These are safe to ignore, since they are built artifacts (insiders)." + }, + { + "file": [ + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/stage/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/stage/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/prime/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/prime/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/build/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/install/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/src/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/build/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/install/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/src/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js" + ], + "_justification": "These are safe to ignore, since they are built artifacts (exploration)." + }, + { + "file": [ + ".build/web/extensions/github-authentication/dist/browser/extension.js", + ".build/web/extensions/emmet/dist/browser/emmetBrowserMain.js.map", + ".build/web/extensions/emmet/dist/browser/emmetBrowserMain.js" + ], + "_justification": "These are safe to ignore, since they are built artifacts (web)." } ] } diff --git a/build/azure-pipelines/config/tsaoptions.json b/build/azure-pipelines/config/tsaoptions.json deleted file mode 100644 index fa8e182d8f3..00000000000 --- a/build/azure-pipelines/config/tsaoptions.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "codebaseName": "devdiv_vscode-client", - "ppe": false, - "notificationAliases": [ - "sbatten@microsoft.com" - ], - "codebaseAdmins": [ - "REDMOND\\stbatt", - "REDMOND\\monacotools" - ], - "instanceUrl": "https://devdiv.visualstudio.com/defaultcollection", - "projectName": "DevDiv", - "areaPath": "DevDiv\\VS Code (compliance tracking only)\\Visual Studio Code Client", - "notifyAlways": true, - "template": "TFSDEVDIV", - "tools": [ - "BinSkim", - "CredScan", - "CodeQL" - ] -} diff --git a/build/azure-pipelines/darwin/cli-build-darwin.yml b/build/azure-pipelines/darwin/cli-build-darwin.yml index ac5adaec175..1d8dffc464d 100644 --- a/build/azure-pipelines/darwin/cli-build-darwin.yml +++ b/build/azure-pipelines/darwin/cli-build-darwin.yml @@ -19,7 +19,7 @@ steps: nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - template: ../cli/cli-apply-patches.yml + - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 displayName: Download openssl prebuilt @@ -36,7 +36,7 @@ steps: tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.11.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl displayName: Extract openssl prebuilt - - template: ../cli/install-rust-posix.yml + - template: ../cli/install-rust-posix.yml@self parameters: targets: - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: @@ -45,7 +45,7 @@ steps: - aarch64-apple-darwin - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_TARGET: x86_64-apple-darwin @@ -56,7 +56,7 @@ steps: OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-osx/include - ${{ if eq(parameters.VSCODE_BUILD_MACOS_ARM64, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_TARGET: aarch64-apple-darwin @@ -66,14 +66,23 @@ steps: OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-osx/lib OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-osx/include - - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_darwin_x64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + - ${{ if not(parameters.VSCODE_CHECK_ONLY) }}: + - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_x64_cli.zip + artifactName: unsigned_vscode_cli_darwin_x64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code macOS x64 CLI (unsigned)" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish unsigned_vscode_cli_darwin_x64_cli artifact - - ${{ if eq(parameters.VSCODE_BUILD_MACOS_ARM64, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_darwin_arm64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + - ${{ if eq(parameters.VSCODE_BUILD_MACOS_ARM64, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_arm64_cli.zip + artifactName: unsigned_vscode_cli_darwin_arm64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code macOS arm64 CLI (unsigned)" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish unsigned_vscode_cli_darwin_arm64_cli artifact diff --git a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml index 6f4132f45ff..80e90a52bac 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml @@ -46,7 +46,7 @@ steps: workingDirectory: build displayName: Install build dependencies - - template: ../cli/cli-darwin-sign.yml + - template: ../cli/cli-darwin-sign.yml@self parameters: VSCODE_CLI_ARTIFACTS: - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: diff --git a/build/azure-pipelines/darwin/product-build-darwin-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-sign.yml index 3f31ac7bd35..fb8cf8341c3 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-sign.yml @@ -32,7 +32,6 @@ steps: - script: unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) displayName: Extract signed app - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - script: | set -e @@ -58,5 +57,11 @@ steps: displayName: Rename x64 build to its legacy name condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) - - publish: $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-$(ASSET_ID).zip - artifact: vscode_client_darwin_$(VSCODE_ARCH)_archive + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-$(ASSET_ID).zip + artifactName: vscode_client_darwin_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH)" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish client archive diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 1ca8c9ec1a9..ed6d0236516 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -155,7 +155,7 @@ steps: condition: succeededOrFailed() - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - task: PublishPipelineArtifact@0 + - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: .build/crashes ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -164,13 +164,14 @@ steps: artifactName: crash-dump-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: crash-dump-macos-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Crash Reports" continueOnError: true condition: failed() # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - - task: PublishPipelineArtifact@0 + - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: node_modules ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -179,11 +180,12 @@ steps: artifactName: node-modules-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: node-modules-macos-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Node Modules" continueOnError: true condition: failed() - - task: PublishPipelineArtifact@0 + - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: .build/logs ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -192,6 +194,7 @@ steps: artifactName: logs-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: logs-macos-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Log Files" continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index 1c21bb778ce..f8b201f40d4 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -5,7 +5,7 @@ steps: versionFilePath: .nvmrc nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - template: ../distro/download-distro.yml + - template: ../distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -82,6 +82,11 @@ steps: - script: pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip * && popd displayName: Archive build - - publish: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip - artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip + artifactName: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH) (unsigned)" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish client archive diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 6bd68712c9d..11aa7605f63 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -23,7 +23,7 @@ steps: nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - template: ../distro/download-distro.yml + - template: ../distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -114,7 +114,7 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality - - template: ../common/install-builtin-extensions.yml + - template: ../common/install-builtin-extensions.yml@self - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | @@ -156,7 +156,7 @@ steps: displayName: Transpile - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - template: product-build-darwin-test.yml + - template: product-build-darwin-test.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} @@ -176,8 +176,7 @@ steps: APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)" APP_NAME="`ls $APP_ROOT | head -n 1`" APP_PATH="$APP_ROOT/$APP_NAME" - ARCHIVE_NAME=$(ls "$(Build.ArtifactStagingDirectory)/cli" | head -n 1) - unzip "$(Build.ArtifactStagingDirectory)/cli/$ARCHIVE_NAME" -d "$(Build.ArtifactStagingDirectory)/cli" + unzip $(Build.ArtifactStagingDirectory)/cli/*.zip -d $(Build.ArtifactStagingDirectory)/cli CLI_APP_NAME=$(node -p "require(\"$APP_PATH/Contents/Resources/app/product.json\").tunnelApplicationName") APP_NAME=$(node -p "require(\"$APP_PATH/Contents/Resources/app/product.json\").applicationName") mv "$(Build.ArtifactStagingDirectory)/cli/$APP_NAME" "$APP_PATH/Contents/Resources/app/bin/$CLI_APP_NAME" @@ -212,38 +211,31 @@ steps: condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) displayName: Generate artifact prefix - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM (client) + - task: 1ES.PublishPipelineArtifact@1 inputs: - BuildDropPath: $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - PackageName: Visual Studio Code - - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM (server) - inputs: - BuildComponentPath: $(Build.SourcesDirectory)/remote - BuildDropPath: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH) - PackageName: Visual Studio Code Server - - - publish: $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM (client) - artifact: $(ARTIFACT_PREFIX)sbom_vscode_client_darwin_$(VSCODE_ARCH) - - - publish: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM (server) - artifact: $(ARTIFACT_PREFIX)sbom_vscode_server_darwin_$(VSCODE_ARCH) - - - publish: $(CLIENT_PATH) - artifact: $(ARTIFACT_PREFIX)unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive - condition: and(succeededOrFailed(), ne(variables['CLIENT_PATH'], '')) + targetPath: $(CLIENT_PATH) + artifactName: $(ARTIFACT_PREFIX)unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH) (unsigned)" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish client archive - - publish: $(SERVER_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_server_darwin_$(VSCODE_ARCH)_archive-unsigned + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SERVER_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_server_darwin_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH) Server" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) displayName: Publish server archive - - publish: $(WEB_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_web_darwin_$(VSCODE_ARCH)_archive-unsigned + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_darwin_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web + sbomPackageName: "VS Code macOS $(VSCODE_ARCH) Web" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) displayName: Publish web server archive diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index c0a8e354c7b..ee5dd5d9919 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -12,4 +12,4 @@ steps: versionSource: fromFile versionFilePath: .nvmrc nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - template: ./distro/download-distro.yml + - template: ./distro/download-distro.yml@self diff --git a/build/azure-pipelines/linux/cli-build-linux.yml b/build/azure-pipelines/linux/cli-build-linux.yml index ff851c63c96..f3e2ef88b9d 100644 --- a/build/azure-pipelines/linux/cli-build-linux.yml +++ b/build/azure-pipelines/linux/cli-build-linux.yml @@ -22,7 +22,7 @@ steps: nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - template: ../cli/cli-apply-patches.yml + - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 displayName: Download openssl prebuilt @@ -79,7 +79,7 @@ steps: mkdir -p $(Build.SourcesDirectory)/.build displayName: Create .build folder for misc dependencies - - template: ../cli/install-rust-posix.yml + - template: ../cli/install-rust-posix.yml@self parameters: targets: - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64, true) }}: @@ -90,7 +90,7 @@ steps: - armv7-unknown-linux-gnueabihf - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_TARGET: aarch64-unknown-linux-gnu @@ -102,7 +102,7 @@ steps: SYSROOT_ARCH: arm64 - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_TARGET: x86_64-unknown-linux-gnu @@ -114,7 +114,7 @@ steps: SYSROOT_ARCH: amd64 - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_TARGET: armv7-unknown-linux-gnueabihf @@ -125,20 +125,33 @@ steps: OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm-linux/include SYSROOT_ARCH: armhf - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: vscode_cli_linux_armhf_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} - - - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: vscode_cli_linux_x64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} - - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: vscode_cli_linux_arm64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + - ${{ if not(parameters.VSCODE_CHECK_ONLY) }}: + - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/vscode_cli_linux_armhf_cli.tar.gz + artifactName: vscode_cli_linux_armhf_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code Linux armhf CLI" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish vscode_cli_linux_armhf_cli artifact + + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/vscode_cli_linux_x64_cli.tar.gz + artifactName: vscode_cli_linux_x64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code Linux x64 CLI" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish vscode_cli_linux_x64_cli artifact + + - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/vscode_cli_linux_arm64_cli.tar.gz + artifactName: vscode_cli_linux_arm64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code Linux arm64 CLI" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish vscode_cli_linux_arm64_cli artifact diff --git a/build/azure-pipelines/linux/install.sh b/build/azure-pipelines/linux/install.sh deleted file mode 100755 index 57f58763cca..00000000000 --- a/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/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml new file mode 100644 index 00000000000..dc8424f26ee --- /dev/null +++ b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -0,0 +1,223 @@ +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-legacy-$(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=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-legacy-$(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=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 + ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 + + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SERVER_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_server_linux_legacy_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH) + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Legacy Server" + sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) + displayName: Publish server archive + + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_linux_legacy_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)-web + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Legacy Web" + sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) + displayName: Publish web server archive diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index 74ebb91e344..f5c00aa0cf0 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -7,6 +7,9 @@ parameters: type: boolean - name: VSCODE_RUN_SMOKE_TESTS type: boolean + - name: PUBLISH_TASK_NAME + type: string + default: PublishPipelineArtifact@0 steps: - script: yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" @@ -197,7 +200,7 @@ steps: condition: succeededOrFailed() - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - task: PublishPipelineArtifact@0 + - task: ${{ parameters.PUBLISH_TASK_NAME }} inputs: targetPath: .build/crashes ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -206,13 +209,14 @@ steps: artifactName: crash-dump-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: crash-dump-linux-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Crash Reports" continueOnError: true condition: failed() # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - - task: PublishPipelineArtifact@0 + - task: ${{ parameters.PUBLISH_TASK_NAME }} inputs: targetPath: node_modules ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -221,11 +225,12 @@ steps: artifactName: node-modules-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: node-modules-linux-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Node Modules" continueOnError: true condition: failed() - - task: PublishPipelineArtifact@0 + - task: ${{ parameters.PUBLISH_TASK_NAME }} inputs: targetPath: .build/logs ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -234,6 +239,7 @@ steps: artifactName: logs-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: logs-linux-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Log Files" continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index e4b4fab899b..cdc687fe7ac 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -25,7 +25,7 @@ steps: nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - template: ../distro/download-distro.yml + - template: ../distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -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')) @@ -180,7 +185,7 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality - - template: ../common/install-builtin-extensions.yml + - template: ../common/install-builtin-extensions.yml@self - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | @@ -218,7 +223,6 @@ steps: - 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" @@ -231,7 +235,6 @@ steps: - 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" @@ -249,12 +252,14 @@ steps: displayName: Transpile client and extensions - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - template: product-build-linux-test.yml + - template: product-build-linux-test.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} + ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - script: | @@ -308,53 +313,60 @@ steps: condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) displayName: Generate artifact prefix - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM (client) - inputs: - BuildDropPath: $(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - PackageName: Visual Studio Code - - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM (server) + - task: 1ES.PublishPipelineArtifact@1 inputs: - BuildComponentPath: $(Build.SourcesDirectory)/remote - BuildDropPath: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) - PackageName: Visual Studio Code Server - - - publish: $(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM (client) - artifact: $(ARTIFACT_PREFIX)sbom_vscode_client_linux_$(VSCODE_ARCH) - - - publish: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM (server) - artifact: $(ARTIFACT_PREFIX)sbom_vscode_server_linux_$(VSCODE_ARCH) - - - publish: $(CLIENT_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_client_linux_$(VSCODE_ARCH)_archive-unsigned + targetPath: $(CLIENT_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_linux_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-linux-$(VSCODE_ARCH) + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) (unsigned)" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['CLIENT_PATH'], '')) displayName: Publish client archive - - publish: $(SERVER_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_server_linux_$(VSCODE_ARCH)_archive-unsigned + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SERVER_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_server_linux_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH) + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Server" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) displayName: Publish server archive - - publish: $(WEB_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_web_linux_$(VSCODE_ARCH)_archive-unsigned + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_linux_$(VSCODE_ARCH)_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)-web + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Web" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) displayName: Publish web server archive - - publish: $(DEB_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_client_linux_$(VSCODE_ARCH)_deb-package + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(DEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_linux_$(VSCODE_ARCH)_deb-package + sbomBuildDropPath: .build/linux/deb + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) DEB" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['DEB_PATH'], '')) displayName: Publish deb package - - publish: $(RPM_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_client_linux_$(VSCODE_ARCH)_rpm-package + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(RPM_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_linux_$(VSCODE_ARCH)_rpm-package + sbomBuildDropPath: .build/linux/rpm + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) RPM" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['RPM_PATH'], '')) displayName: Publish rpm package - - publish: $(SNAP_PATH) - artifact: $(ARTIFACT_PREFIX)snap-$(VSCODE_ARCH) + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SNAP_PATH) + artifactName: $(ARTIFACT_PREFIX)snap-$(VSCODE_ARCH) + sbomEnabled: false condition: and(succeededOrFailed(), ne(variables['SNAP_PATH'], '')) displayName: Publish snap pre-package diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh new file mode 100755 index 00000000000..e42a6b12b1f --- /dev/null +++ b/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/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index 6fc16833921..033058163f9 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -46,24 +46,28 @@ steps: *) SNAPCRAFT_TARGET_ARGS="--target-arch $(VSCODE_ARCH)" ;; esac (cd $SNAP_ROOT/code-* && sudo --preserve-env snapcraft snap $SNAPCRAFT_TARGET_ARGS --output "$SNAP_PATH") - - # Export SNAP_PATH - echo "##vso[task.setvariable variable=SNAP_PATH]$SNAP_PATH" displayName: Prepare for publish - - script: mkdir -p $(agent.builddirectory)/vscode-snap-linux-$(VSCODE_ARCH) - displayName: Make folder for SBOM + - script: | + set -e + SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)" + SNAP_EXTRACTED_PATH=$(find $SNAP_ROOT -maxdepth 1 -type d -name 'code-*') + SNAP_PATH=$(find $SNAP_ROOT -maxdepth 1 -type f -name '*.snap') - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM - inputs: - BuildDropPath: $(agent.builddirectory)/vscode-snap-linux-$(VSCODE_ARCH) - PackageName: Visual Studio Code Snap + # SBOM tool doesn't like recursive symlinks + sudo find $SNAP_EXTRACTED_PATH -type l -delete - - publish: $(agent.builddirectory)/vscode-snap-linux-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM - artifact: $(ARTIFACT_PREFIX)sbom_vscode_client_linux_snap_$(VSCODE_ARCH) + echo "##vso[task.setvariable variable=SNAP_EXTRACTED_PATH]$SNAP_EXTRACTED_PATH" + echo "##vso[task.setvariable variable=SNAP_PATH]$SNAP_PATH" + target: + container: host + displayName: Find host snap path & prepare for SBOM - - publish: $(SNAP_PATH) - artifact: vscode_client_linux_$(VSCODE_ARCH)_snap + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SNAP_PATH) + artifactName: vscode_client_linux_$(VSCODE_ARCH)_snap + sbomBuildDropPath: $(SNAP_EXTRACTED_PATH) + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) SNAP" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish snap package diff --git a/build/azure-pipelines/product-build-pr.yml b/build/azure-pipelines/product-build-pr.yml index b5036070730..7dce4d20265 100644 --- a/build/azure-pipelines/product-build-pr.yml +++ b/build/azure-pipelines/product-build-pr.yml @@ -31,7 +31,7 @@ jobs: variables: VSCODE_ARCH: x64 steps: - - template: product-compile.yml + - template: product-compile.yml@self parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} @@ -44,7 +44,7 @@ jobs: NPM_ARCH: x64 DISPLAY: ":10" steps: - - template: linux/product-build-linux.yml + - template: linux/product-build-linux.yml@self parameters: VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} @@ -62,7 +62,7 @@ jobs: NPM_ARCH: x64 DISPLAY: ":10" steps: - - template: linux/product-build-linux.yml + - template: linux/product-build-linux.yml@self parameters: VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} @@ -80,7 +80,7 @@ jobs: NPM_ARCH: x64 DISPLAY: ":10" steps: - - template: linux/product-build-linux.yml + - template: linux/product-build-linux.yml@self parameters: VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} @@ -94,7 +94,7 @@ jobs: pool: 1es-oss-ubuntu-20.04-x64 timeoutInMinutes: 30 steps: - - template: cli/test.yml + - template: cli/test.yml@self - job: Windowsx64UnitTests displayName: Windows (Unit Tests) @@ -104,7 +104,7 @@ jobs: VSCODE_ARCH: x64 NPM_ARCH: x64 steps: - - template: win32/product-build-win32.yml + - template: win32/product-build-win32.yml@self parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 @@ -121,7 +121,7 @@ jobs: VSCODE_ARCH: x64 NPM_ARCH: x64 steps: - - template: win32/product-build-win32.yml + - template: win32/product-build-win32.yml@self parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 @@ -138,7 +138,7 @@ jobs: # VSCODE_ARCH: x64 # NPM_ARCH: x64 # steps: - # - template: win32/product-build-win32.yml + # - template: win32/product-build-win32.yml@self # parameters: # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} # VSCODE_ARCH: x64 @@ -154,7 +154,7 @@ jobs: variables: VSCODE_ARCH: x64 steps: - - template: oss/product-build-pr-cache-linux.yml + - template: oss/product-build-pr-cache-linux.yml@self - job: Windowsx64MaintainNodeModulesCache displayName: Windows (Maintain node_modules cache) @@ -163,7 +163,7 @@ jobs: variables: VSCODE_ARCH: x64 steps: - - template: oss/product-build-pr-cache-win32.yml + - template: oss/product-build-pr-cache-win32.yml@self # - job: macOSUnitTest # displayName: macOS (Unit Tests) @@ -174,7 +174,7 @@ jobs: # BUILDSECMON_OPT_IN: true # VSCODE_ARCH: x64 # steps: - # - template: darwin/product-build-darwin.yml + # - template: darwin/product-build-darwin.yml@self # parameters: # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} # VSCODE_RUN_UNIT_TESTS: true @@ -189,7 +189,7 @@ jobs: # BUILDSECMON_OPT_IN: true # VSCODE_ARCH: x64 # steps: - # - template: darwin/product-build-darwin.yml + # - template: darwin/product-build-darwin.yml@self # parameters: # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} # VSCODE_RUN_UNIT_TESTS: false @@ -204,7 +204,7 @@ jobs: # BUILDSECMON_OPT_IN: true # VSCODE_ARCH: x64 # steps: - # - template: darwin/product-build-darwin.yml + # - template: darwin/product-build-darwin.yml@self # parameters: # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} # VSCODE_RUN_UNIT_TESTS: false diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index d6134addf8a..4acf5fd1b15 100644 --- a/build/azure-pipelines/product-build.yml +++ b/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 @@ -142,530 +156,634 @@ variables: name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.VSCODE_QUALITY }})" resources: - containers: - - container: snapcraft - image: vscodehub.azurecr.io/vscode-linux-build-agent@sha256:ab4a88c4d85e0d7a85acabba59543f7143f575bab2c0b2b07f5b77d4a7e491ff - endpoint: VSCodeHub pipelines: - pipeline: vscode-7pm-kick-off source: 'VS Code 7PM Kick-Off' trigger: true - -stages: - - stage: Compile - jobs: - - job: Compile - pool: 1es-ubuntu-20.04-x64 - variables: - VSCODE_ARCH: x64 - steps: - - template: product-compile.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - - - stage: CompileCLI - dependsOn: [] - jobs: - - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: - - job: CLILinuxX64 - pool: 1es-ubuntu-20.04-x64 - steps: - - template: ./linux/cli-build-linux.yml - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_LINUX: ${{ parameters.VSCODE_BUILD_LINUX }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true))) }}: - - job: CLILinuxGnuARM - pool: 1es-ubuntu-20.04-x64 - steps: - - template: ./linux/cli-build-linux.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_LINUX_ARMHF: ${{ parameters.VSCODE_BUILD_LINUX_ARMHF }} - VSCODE_BUILD_LINUX_ARM64: ${{ parameters.VSCODE_BUILD_LINUX_ARM64 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE, true)) }}: - - job: CLIAlpineX64 - pool: 1es-ubuntu-20.04-x64 - steps: - - template: ./alpine/cli-build-alpine.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_ALPINE: ${{ parameters.VSCODE_BUILD_ALPINE }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }}: - - job: CLIAlpineARM64 - pool: 1es-ubuntu-20.04-arm64 - steps: - - bash: sudo apt update && sudo apt install -y unzip - displayName: Install unzip - - template: ./alpine/cli-build-alpine.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_ALPINE_ARM64: ${{ parameters.VSCODE_BUILD_ALPINE_ARM64 }} - - - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - - job: CLIMacOSX64 - pool: - vmImage: macOS-11 - steps: - - template: ./darwin/cli-build-darwin.yml - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - - job: CLIMacOSARM64 - pool: - vmImage: macOS-11 - steps: - - template: ./darwin/cli-build-darwin.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} - - - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - job: CLIWindowsX64 - pool: 1es-windows-2019-x64 - steps: - - template: ./win32/cli-build-win32.yml - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - - job: CLIWindowsARM64 - pool: 1es-windows-2019-x64 - steps: - - template: ./win32/cli-build-win32.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} - - - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true)) }}: - - stage: Windows - dependsOn: - - Compile - - CompileCLI - pool: 1es-windows-2019-x64 - jobs: - - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: WindowsUnitTests - displayName: Unit Tests - timeoutInMinutes: 60 + repositories: + - repository: 1ESPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/heads/joao/disable-tsa-linux-arm64 + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + sdl: + tsa: + enabled: true + config: + codebaseName: 'devdiv_$(Build.Repository.Name)' + serviceTreeID: '79c048b2-322f-4ed5-a1ea-252a1250e4b3' + instanceUrl: 'https://devdiv.visualstudio.com/defaultcollection' + projectName: 'DevDiv' + areaPath: "DevDiv\\VS Code (compliance tracking only)\\Visual Studio Code Client" + notificationAliases: ['monacotools@microsoft.com'] + validateToolOutput: None + allTools: true + credscan: + suppressionsFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/CredScanSuppressions.json + sourceAnalysisPool: 1es-windows-2022-x64 + containers: + snapcraft: + image: vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64 + ubuntu-2004-arm64: + image: onebranch.azurecr.io/linux/ubuntu-2004-arm64:latest + authenticatedContainerRegistries: + - registry: onebranch.azurecr.io + tenant: AME + identity: 1ESPipelineIdentity + stages: + - stage: Compile + jobs: + - job: Compile + pool: + name: 1es-ubuntu-20.04-x64 + os: linux variables: VSCODE_ARCH: x64 steps: - - template: win32/product-build-win32.yml + - template: build/azure-pipelines/product-compile.yml@self parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + + - stage: CompileCLI + dependsOn: [] + jobs: + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - job: CLILinuxX64 + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/linux/cli-build-linux.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_LINUX: ${{ parameters.VSCODE_BUILD_LINUX }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true))) }}: + - job: CLILinuxGnuARM + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/linux/cli-build-linux.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_LINUX_ARMHF: ${{ parameters.VSCODE_BUILD_LINUX_ARMHF }} + VSCODE_BUILD_LINUX_ARM64: ${{ parameters.VSCODE_BUILD_LINUX_ARM64 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE, true)) }}: + - job: CLIAlpineX64 + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_ALPINE: ${{ parameters.VSCODE_BUILD_ALPINE }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }}: + - job: CLIAlpineARM64 + pool: + name: 1es-mariner-2.0-arm64 + os: linux + hostArchitecture: arm64 + container: ubuntu-2004-arm64 + steps: + - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_ALPINE_ARM64: ${{ parameters.VSCODE_BUILD_ALPINE_ARM64 }} + + - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: + - job: CLIMacOSX64 + pool: + name: Azure Pipelines + image: macOS-11 + os: macOS + steps: + - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: + - job: CLIMacOSARM64 + pool: + name: Azure Pipelines + image: macOS-11 + os: macOS + steps: + - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} + + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - job: CLIWindowsX64 + pool: + name: 1es-windows-2019-x64 + os: windows + steps: + - template: build/azure-pipelines/win32/cli-build-win32.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - job: CLIWindowsARM64 + pool: + name: 1es-windows-2019-x64 + os: windows + steps: + - template: build/azure-pipelines/win32/cli-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true)) }}: + - stage: Windows + dependsOn: + - Compile + - CompileCLI + pool: + name: 1es-windows-2019-x64 + os: windows + jobs: + - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: + - job: WindowsUnitTests + displayName: Unit Tests + timeoutInMinutes: 60 + variables: VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: WindowsIntegrationTests - displayName: Integration Tests - timeoutInMinutes: 60 - variables: - VSCODE_ARCH: x64 - steps: - - template: win32/product-build-win32.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + steps: + - template: build/azure-pipelines/win32/product-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_ARCH: x64 + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: true + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + - job: WindowsIntegrationTests + displayName: Integration Tests + timeoutInMinutes: 60 + variables: VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: WindowsSmokeTests - displayName: Smoke Tests - timeoutInMinutes: 60 - variables: - VSCODE_ARCH: x64 - steps: - - template: win32/product-build-win32.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + steps: + - template: build/azure-pipelines/win32/product-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_ARCH: x64 + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: true + VSCODE_RUN_SMOKE_TESTS: false + - job: WindowsSmokeTests + displayName: Smoke Tests + timeoutInMinutes: 60 + variables: VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32, true)) }}: - - job: Windows - timeoutInMinutes: 120 - variables: - VSCODE_ARCH: x64 - steps: - - template: win32/product-build-win32.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + steps: + - template: build/azure-pipelines/win32/product-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_ARCH: x64 + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: true + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32, true)) }}: + - job: Windows + timeoutInMinutes: 120 + variables: VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - job: WindowsCLISign - timeoutInMinutes: 90 - steps: - - template: win32/product-build-win32-cli-sign.yml - parameters: - VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} - VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - - job: WindowsARM64 - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: arm64 - steps: - - template: win32/product-build-win32.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + steps: + - template: build/azure-pipelines/win32/product-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_ARCH: x64 + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + + - job: WindowsCLISign + timeoutInMinutes: 90 + steps: + - template: build/azure-pipelines/win32/product-build-win32-cli-sign.yml@self + parameters: + VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} + VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - job: WindowsARM64 + timeoutInMinutes: 90 + variables: VSCODE_ARCH: arm64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX'], true)) }}: - - stage: Linux - dependsOn: - - Compile - - CompileCLI - pool: 1es-ubuntu-20.04-x64 - jobs: - - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: Linuxx64UnitTest - displayName: Unit Tests - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux.yml - parameters: + steps: + - template: build/azure-pipelines/win32/product-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_ARCH: arm64 + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX'], true)) }}: + - stage: Linux + dependsOn: + - Compile + - CompileCLI + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + jobs: + - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: + - job: Linuxx64UnitTest + displayName: Unit Tests + variables: VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: Linuxx64IntegrationTest - displayName: Integration Tests - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux.yml - parameters: + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: build/azure-pipelines/linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: true + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + - job: Linuxx64IntegrationTest + displayName: Integration Tests + variables: VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: Linuxx64SmokeTest - displayName: Smoke Tests - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux.yml - parameters: + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: build/azure-pipelines/linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: true + VSCODE_RUN_SMOKE_TESTS: false + - job: Linuxx64SmokeTest + displayName: Smoke Tests + variables: VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: - - job: Linuxx64 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux.yml - parameters: + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: build/azure-pipelines/linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: true + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: + - job: Linuxx64 + timeoutInMinutes: 90 + variables: VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: - - job: LinuxSnap - dependsOn: - - Linuxx64 - container: snapcraft - variables: - VSCODE_ARCH: x64 - steps: - - template: linux/snap-build-linux.yml + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: build/azure-pipelines/linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: + - job: LinuxSnap + dependsOn: + - Linuxx64 + container: snapcraft + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/linux/snap-build-linux.yml@self - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true)) }}: - - job: LinuxArmhf - variables: - VSCODE_ARCH: armhf - NPM_ARCH: arm - steps: - - template: linux/product-build-linux.yml - parameters: + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true)) }}: + - job: LinuxArmhf + variables: VSCODE_ARCH: armhf - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: - - job: LinuxArm64 - variables: - VSCODE_ARCH: arm64 - NPM_ARCH: arm64 - steps: - - template: linux/product-build-linux.yml - parameters: + NPM_ARCH: arm + steps: + - template: build/azure-pipelines/linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: armhf + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: + - job: LinuxArm64 + variables: VSCODE_ARCH: arm64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - 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_ALPINE'], true)) }}: - - stage: Alpine - dependsOn: - - Compile - - CompileCLI - pool: 1es-ubuntu-20.04-x64 - jobs: - - ${{ if eq(parameters.VSCODE_BUILD_ALPINE, true) }}: - - job: LinuxAlpine - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - steps: - - template: alpine/product-build-alpine.yml - parameters: - VSCODE_BUILD_ALPINE: ${{ parameters.VSCODE_BUILD_ALPINE }} + NPM_ARCH: arm64 + steps: + - template: build/azure-pipelines/linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: arm64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + 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: + name: 1es-ubuntu-20.04-x64 + os: linux + jobs: + - ${{ if eq(parameters.VSCODE_BUILD_LINUX_X64_LEGACY_SERVER, true) }}: + - job: Linuxx64LegacyServer + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self + 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: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self + 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: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self + 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: + - Compile + - CompileCLI + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + jobs: + - ${{ if eq(parameters.VSCODE_BUILD_ALPINE, true) }}: + - job: LinuxAlpine + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: build/azure-pipelines/alpine/product-build-alpine.yml@self + + - ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}: + - job: LinuxAlpineArm64 + timeoutInMinutes: 120 + variables: + VSCODE_ARCH: arm64 + NPM_ARCH: arm64 + steps: + - template: build/azure-pipelines/alpine/product-build-alpine.yml@self + + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_MACOS'], true)) }}: + - stage: macOS + dependsOn: + - Compile + - CompileCLI + pool: + name: Azure Pipelines + image: macOS-11 + os: macOS + variables: + BUILDSECMON_OPT_IN: true + jobs: + - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: + - job: macOSUnitTest + displayName: Unit Tests + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: true + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + - job: macOSIntegrationTest + displayName: Integration Tests + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: true + VSCODE_RUN_SMOKE_TESTS: false + - job: macOSSmokeTest + displayName: Smoke Tests + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: true + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS, true)) }}: + - job: macOS + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + + - ${{ if eq(parameters.VSCODE_STEP_ON_IT, false) }}: + - job: macOSTest + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + + - job: macOSSign + dependsOn: + - macOS + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin-sign.yml@self + + - job: macOSCLISign + timeoutInMinutes: 90 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml@self + parameters: + VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} + VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: + - job: macOSARM64 + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: arm64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + + - job: macOSARM64Sign + dependsOn: + - macOSARM64 + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: arm64 + steps: + - template: build/azure-pipelines/darwin/product-build-darwin-sign.yml@self + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(variables['VSCODE_BUILD_MACOS_UNIVERSAL'], true)) }}: + - job: macOSUniversal + dependsOn: + - macOS + - macOSARM64 + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: universal + steps: + - template: build/azure-pipelines/darwin/product-build-darwin-universal.yml@self + + - job: macOSUniversalSign + dependsOn: + - macOSUniversal + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: universal + steps: + - template: build/azure-pipelines/darwin/product-build-darwin-sign.yml@self + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WEB'], true)) }}: + - stage: Web + dependsOn: + - Compile + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + jobs: + - ${{ if eq(parameters.VSCODE_BUILD_WEB, true) }}: + - job: Web + variables: + VSCODE_ARCH: x64 + steps: + - template: build/azure-pipelines/web/product-build-web.yml@self - - ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}: - - job: LinuxAlpineArm64 - timeoutInMinutes: 120 - variables: - VSCODE_ARCH: arm64 - NPM_ARCH: arm64 - steps: - - template: alpine/product-build-alpine.yml - parameters: - VSCODE_BUILD_ALPINE_ARM64: ${{ parameters.VSCODE_BUILD_ALPINE_ARM64 }} - - - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_MACOS'], true)) }}: - - stage: macOS - dependsOn: - - Compile - - CompileCLI - pool: - vmImage: macOS-11 - variables: - BUILDSECMON_OPT_IN: true - jobs: - - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: macOSUnitTest - displayName: Unit Tests - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: macOSIntegrationTest - displayName: Integration Tests - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: macOSSmokeTest - displayName: Smoke Tests - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS, true)) }}: - - job: macOS - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - ${{ if eq(parameters.VSCODE_STEP_ON_IT, false) }}: - - job: macOSTest - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 + - ${{ if eq(variables['VSCODE_PUBLISH'], 'true') }}: + - stage: Publish + dependsOn: [] + pool: + name: 1es-windows-2019-x64 + os: windows + variables: + - name: BUILDS_API_URL + value: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ + jobs: + - job: PublishBuild + timeoutInMinutes: 180 + displayName: Publish Build steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - job: macOSSign + - template: build/azure-pipelines/product-publish.yml@self + + - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: + - stage: ApproveRelease + dependsOn: [] # run in parallel to compile stage + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + jobs: + - deployment: ApproveRelease + displayName: "Approve Release" + environment: "vscode" + variables: + skipComponentGovernanceDetection: true + strategy: + runOnce: + deploy: + steps: + - checkout: none + + - ${{ if or(and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: + - stage: Release dependsOn: - - macOS - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin-sign.yml - - - job: macOSCLISign - timeoutInMinutes: 90 - steps: - - template: darwin/product-build-darwin-cli-sign.yml - parameters: - VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} - VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - - job: macOSARM64 - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: arm64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - job: macOSARM64Sign - dependsOn: - - macOSARM64 - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: arm64 - steps: - - template: darwin/product-build-darwin-sign.yml - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(variables['VSCODE_BUILD_MACOS_UNIVERSAL'], true)) }}: - - job: macOSUniversal - dependsOn: - - macOS - - macOSARM64 - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: universal - steps: - - template: darwin/product-build-darwin-universal.yml - - - job: macOSUniversalSign - dependsOn: - - macOSUniversal - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: universal - steps: - - template: darwin/product-build-darwin-sign.yml - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WEB'], true)) }}: - - stage: Web - dependsOn: - - Compile - pool: 1es-ubuntu-20.04-x64 - jobs: - - ${{ if eq(parameters.VSCODE_BUILD_WEB, true) }}: - - job: Web - variables: - VSCODE_ARCH: x64 - steps: - - template: web/product-build-web.yml - - - ${{ if eq(variables['VSCODE_PUBLISH'], 'true') }}: - - stage: Publish - dependsOn: [] - pool: 1es-windows-2019-x64 - variables: - - name: BUILDS_API_URL - value: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ - jobs: - - job: PublishBuild - timeoutInMinutes: 180 - displayName: Publish Build - steps: - - template: product-publish.yml - - - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: - - stage: ApproveRelease - dependsOn: [] # run in parallel to compile stage - pool: 1es-ubuntu-20.04-x64 - jobs: - - deployment: ApproveRelease - displayName: "Approve Release" - environment: "vscode" - variables: - skipComponentGovernanceDetection: true - strategy: - runOnce: - deploy: - steps: - - checkout: none - - - ${{ if or(and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: - - stage: Release - dependsOn: - - Publish - - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: - - ApproveRelease - pool: 1es-ubuntu-20.04-x64 - jobs: - - job: ReleaseBuild - displayName: Release Build - steps: - - template: product-release.yml - parameters: - VSCODE_RELEASE: ${{ parameters.VSCODE_RELEASE }} + - Publish + - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: + - ApproveRelease + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + jobs: + - job: ReleaseBuild + displayName: Release Build + steps: + - template: build/azure-pipelines/product-release.yml@self + parameters: + VSCODE_RELEASE: ${{ parameters.VSCODE_RELEASE }} diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index ac95819f7a9..5fd12caf017 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -10,7 +10,7 @@ steps: nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - template: ./distro/download-distro.yml + - template: ./distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -98,7 +98,7 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality - - template: common/install-builtin-extensions.yml + - template: common/install-builtin-extensions.yml@self - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - script: yarn npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check @@ -146,10 +146,11 @@ steps: - script: tar -cz --ignore-failed-read --exclude='.build/node_modules_cache' --exclude='.build/node_modules_list.txt' --exclude='.build/distro' -f $(Build.ArtifactStagingDirectory)/compilation.tar.gz .build out-* test/integration/browser/out test/smoke/out test/automation/out displayName: Compress compilation artifact - - task: PublishPipelineArtifact@1 + - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: $(Build.ArtifactStagingDirectory)/compilation.tar.gz artifactName: Compilation + sbomEnabled: false displayName: Publish compilation artifact - script: yarn download-builtin-extensions-cg diff --git a/build/azure-pipelines/product-onebranch.yml b/build/azure-pipelines/product-onebranch.yml deleted file mode 100644 index 6241e0c0ee4..00000000000 --- a/build/azure-pipelines/product-onebranch.yml +++ /dev/null @@ -1,46 +0,0 @@ -trigger: none -pr: none - -variables: - LinuxContainerImage: "onebranch.azurecr.io/linux/ubuntu-2004:latest" - -resources: - repositories: - - repository: templates - type: git - name: OneBranch.Pipelines/GovernedTemplates - ref: refs/heads/main - - - repository: distro - type: github - name: microsoft/vscode-distro - ref: refs/heads/distro - endpoint: Monaco - -extends: - template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates - parameters: - git: - fetchDepth: 1 - lfs: true - retryCount: 3 - - globalSdl: - policheck: - break: true - credscan: - suppressionsFile: $(Build.SourcesDirectory)\build\azure-pipelines\config\CredScanSuppressions.json - - stages: - - stage: Compile - - jobs: - - job: Compile - pool: - type: linux - - variables: - ob_outputDirectory: '$(Build.SourcesDirectory)' - - steps: - - checkout: distro diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 1cf0209aa63..2c57e131c1a 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -101,8 +101,11 @@ steps: displayName: Process artifacts retryCountOnTaskFailure: 3 - - publish: $(Pipeline.Workspace)/artifacts_processed_$(System.StageAttempt)/artifacts_processed_$(System.StageAttempt).txt - artifact: artifacts_processed_$(System.StageAttempt) + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Pipeline.Workspace)/artifacts_processed_$(System.StageAttempt)/artifacts_processed_$(System.StageAttempt).txt + artifactName: artifacts_processed_$(System.StageAttempt) + sbomEnabled: false displayName: Publish the artifacts processed for this stage attempt condition: always() @@ -113,6 +116,7 @@ steps: $stages = @( if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } + if ($env:VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER -eq 'True') { 'LinuxLegacyServer' } if ($env:VSCODE_BUILD_STAGE_ALPINE -eq 'True') { 'Alpine' } if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } if ($env:VSCODE_BUILD_STAGE_WEB -eq 'True') { 'Web' } diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 1cff98f82e1..72ded6bcc11 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -5,7 +5,7 @@ steps: versionFilePath: .nvmrc nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - template: ../distro/download-distro.yml + - template: ../distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -94,7 +94,7 @@ steps: - script: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality - - template: ../common/install-builtin-extensions.yml + - template: ../common/install-builtin-extensions.yml@self - script: | set -e @@ -153,17 +153,12 @@ steps: condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) displayName: Generate artifact prefix - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM + - task: 1ES.PublishPipelineArtifact@1 inputs: - BuildDropPath: $(agent.builddirectory)/vscode-web - PackageName: Visual Studio Code Web - - - publish: $(agent.builddirectory)/vscode-web/_manifest - displayName: Publish SBOM (client) - artifact: $(ARTIFACT_PREFIX)sbom_vscode_web - - - publish: $(WEB_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_web_linux_standalone_archive-unsigned + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_linux_standalone_archive-unsigned + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-web + sbomPackageName: "VS Code Web" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) displayName: Publish web archive diff --git a/build/azure-pipelines/win32/cli-build-win32.yml b/build/azure-pipelines/win32/cli-build-win32.yml index 1210b1555c0..19409272ff0 100644 --- a/build/azure-pipelines/win32/cli-build-win32.yml +++ b/build/azure-pipelines/win32/cli-build-win32.yml @@ -19,7 +19,7 @@ steps: nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - template: ../cli/cli-apply-patches.yml + - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 displayName: Download openssl prebuilt @@ -35,7 +35,7 @@ steps: tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.11.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl displayName: Extract openssl prebuilt - - template: ../cli/install-rust-win32.yml + - template: ../cli/install-rust-win32.yml@self parameters: targets: - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: @@ -44,7 +44,7 @@ steps: - aarch64-pc-windows-msvc - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_TARGET: x86_64-pc-windows-msvc @@ -56,7 +56,7 @@ steps: RUSTFLAGS: "-C target-feature=+crt-static" - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: - - template: ../cli/cli-compile.yml + - template: ../cli/cli-compile.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_TARGET: aarch64-pc-windows-msvc @@ -67,14 +67,23 @@ steps: OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-windows-static/include RUSTFLAGS: "-C target-feature=+crt-static" - - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_arm64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + - ${{ if not(parameters.VSCODE_CHECK_ONLY) }}: + - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_arm64_cli.zip + artifactName: unsigned_vscode_cli_win32_arm64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code Windows arm64 CLI (unsigned)" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish unsigned_vscode_cli_win32_arm64_cli artifact - - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - template: ../cli/cli-publish.yml - parameters: - VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_x64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_x64_cli.zip + artifactName: unsigned_vscode_cli_win32_x64_cli + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli + sbomPackageName: "VS Code Windows x64 CLI (unsigned)" + sbomPackageVersion: $(Build.SourceVersion) + displayName: Publish unsigned_vscode_cli_win32_x64_cli artifact diff --git a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml index 75b855288b0..3b5668d0082 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml @@ -44,7 +44,7 @@ steps: workingDirectory: build displayName: Install build dependencies - - template: ../cli/cli-win32-sign.yml + - template: ../cli/cli-win32-sign.yml@self parameters: VSCODE_CLI_ARTIFACTS: - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index cc9867ef4fc..a3b251b71ac 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -9,6 +9,9 @@ parameters: type: boolean - name: VSCODE_RUN_SMOKE_TESTS type: boolean + - name: PUBLISH_TASK_NAME + type: string + default: PublishPipelineArtifact@0 steps: - powershell: yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" @@ -162,7 +165,7 @@ steps: condition: succeededOrFailed() - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - task: PublishPipelineArtifact@0 + - task: ${{ parameters.PUBLISH_TASK_NAME }} inputs: targetPath: .build\crashes ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -171,13 +174,14 @@ steps: artifactName: crash-dump-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: crash-dump-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Crash Reports" continueOnError: true condition: failed() # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - - task: PublishPipelineArtifact@0 + - task: ${{ parameters.PUBLISH_TASK_NAME }} inputs: targetPath: node_modules ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -186,11 +190,12 @@ steps: artifactName: node-modules-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Node Modules" continueOnError: true condition: failed() - - task: PublishPipelineArtifact@0 + - task: ${{ parameters.PUBLISH_TASK_NAME }} inputs: targetPath: .build\logs ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: @@ -199,6 +204,7 @@ steps: artifactName: logs-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) ${{ else }}: artifactName: logs-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + sbomEnabled: false displayName: "Publish Log Files" continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index ed316e721bc..3c92499b2a6 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -30,7 +30,7 @@ steps: addToPath: true - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - template: ../distro/download-distro.yml + - template: ../distro/download-distro.yml@self - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -127,7 +127,7 @@ steps: - powershell: node build/azure-pipelines/distro/mixin-quality displayName: Mixin distro quality - - template: ../common/install-builtin-extensions.yml + - template: ../common/install-builtin-extensions.yml@self - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - powershell: node build\lib\policies @@ -180,13 +180,15 @@ steps: condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - template: product-build-win32-test.yml + - template: product-build-win32-test.yml@self parameters: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} + ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: @@ -299,50 +301,52 @@ steps: condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) displayName: Generate artifact prefix - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM (client) + - task: 1ES.PublishPipelineArtifact@1 inputs: - BuildDropPath: $(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH) - PackageName: Visual Studio Code - - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: Generate SBOM (server) - inputs: - BuildComponentPath: $(Build.SourcesDirectory)/remote - BuildDropPath: $(agent.builddirectory)/vscode-server-win32-$(VSCODE_ARCH) - PackageName: Visual Studio Code Server - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - - - publish: $(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM (client) - artifact: $(ARTIFACT_PREFIX)sbom_vscode_client_win32_$(VSCODE_ARCH) - - - publish: $(agent.builddirectory)/vscode-server-win32-$(VSCODE_ARCH)/_manifest - displayName: Publish SBOM (server) - artifact: $(ARTIFACT_PREFIX)sbom_vscode_server_win32_$(VSCODE_ARCH) - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - - - publish: $(CLIENT_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_archive + targetPath: $(CLIENT_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH)" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['CLIENT_PATH'], '')) displayName: Publish archive - - publish: $(SERVER_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_server_win32_$(VSCODE_ARCH)_archive + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SERVER_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_server_win32_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Server" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], ''), ne(variables['VSCODE_ARCH'], 'arm64')) displayName: Publish server archive - - publish: $(WEB_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_web_win32_$(VSCODE_ARCH)_archive + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_win32_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)-web + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Web" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], ''), ne(variables['VSCODE_ARCH'], 'arm64')) displayName: Publish web server archive - - publish: $(SYSTEM_SETUP_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_setup + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(SYSTEM_SETUP_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_setup + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) System Setup" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['SYSTEM_SETUP_PATH'], '')) displayName: Publish system setup - - publish: $(USER_SETUP_PATH) - artifact: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_user-setup + - task: 1ES.PublishPipelineArtifact@1 + inputs: + targetPath: $(USER_SETUP_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_user-setup + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) User Setup" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['USER_SETUP_PATH'], '')) displayName: Publish user setup diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index a774dffc830..86f78d0adea 100644 --- a/build/checksums/electron.txt +++ b/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 +69b40637a88ad4c17877b3d665b39ad0e11928aa71b19ef45f5b76250d1c9786 *chromedriver-v28.2.8-darwin-arm64.zip +3a9ce6179228245f2c7878c4238e10d51c77dc20642922a226ccc235a20f5a29 *chromedriver-v28.2.8-darwin-x64.zip +7f6470ea5d86dbe68fcc3fccfefd3b7135ba3468ef54b0235bf57cedeabf433d *chromedriver-v28.2.8-linux-arm64.zip +4bfe709d58b237f5c5a7618b2abecf533dac9415d327e763ad6cf622218517cc *chromedriver-v28.2.8-linux-armv7l.zip +7558ee413f96f88b9b9ad5787dd433adcfaf56411fdf052826d39d204ebaba9d *chromedriver-v28.2.8-linux-x64.zip +9814583b075d969c32afb6e929b4bf7956b0223fded996c91341388b8f638dd6 *chromedriver-v28.2.8-mas-arm64.zip +82d11c6606db9aea355b1e410083c72bd1e39abb9e34a839c16b16b75364ea0d *chromedriver-v28.2.8-mas-x64.zip +4803a5335a40ba208136094f5adfde2c4272761d34e0e9e9f4febc2ef676c3ad *chromedriver-v28.2.8-win32-arm64.zip +7b079f47869f7e96a5829f6fb7eff032394f76218b39a2aaf73cc93ce8a68050 *chromedriver-v28.2.8-win32-ia32.zip +2aedd176d4f72b29cd1914364e813756d52f53558df32e3429996b820edc994d *chromedriver-v28.2.8-win32-x64.zip +ae1a521aa36053a3b60b318d7bc093ec7579af6aa8b02bffe1f9e70d6922b726 *electron-api.json +a916f0cc438258f42f43955157565e7eca14966266f3fb123c8c736bece97daa *electron-v28.2.8-darwin-arm64-dsym-snapshot.zip +3c31d0a105b0632f15aa8adc68f06dc8ca47b1fdf1e62d1436ac43af117a22fb *electron-v28.2.8-darwin-arm64-dsym.zip +dab03f1cd7b499552d503bcca2fc1c3f40a1d2c463655ca3ace20778f08e9b04 *electron-v28.2.8-darwin-arm64-symbols.zip +2965d8c8d64fb6c51f5a283a246de653bfae22fe4bf9adf6c04592afabf62f04 *electron-v28.2.8-darwin-arm64.zip +03511a34d94d27eb576ab20e3a432c082a32a298475c7a85a329e029dddc55e4 *electron-v28.2.8-darwin-x64-dsym-snapshot.zip +96089786bd2723786673561c9b6f9a154928de663f2411f10153e6c985703eef *electron-v28.2.8-darwin-x64-dsym.zip +872789c3c218ab8f98be83c7781e3e6ef0114bd39780d65eaae77e99dbbda1de *electron-v28.2.8-darwin-x64-symbols.zip +a7889addd37254f842798bdd3ca34752b75acf6d8dd456cdeb2d75590c0a9ceb *electron-v28.2.8-darwin-x64.zip +fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-arm64-debug.zip +591248f7c94a6d7c4a4d8b2fcf63c8e4347018a65e1f68ed90e5549a587062c8 *electron-v28.2.8-linux-arm64-symbols.zip +6183db1029cebd9e0fb0e4f2d24a80b0274c5265756e66cb9fa0a480b92c98ea *electron-v28.2.8-linux-arm64.zip +fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-armv7l-debug.zip +87c4c534cd1d447b9d4632585a0d79c9d31114bd39ca63df1f2384afae3aa6b7 *electron-v28.2.8-linux-armv7l-symbols.zip +2a772b65815a0d47a756eed52f76cd9f27a8c277d7998bfcfe93b84a346eb255 *electron-v28.2.8-linux-armv7l.zip +773aa1f0bbe2b79765bf498958565f63957f8ec2e42327978a143dcbbc7f1bea *electron-v28.2.8-linux-x64-debug.zip +f8cbc6f2b719cc2f623afcfde8cb1d42614708793621a7a97b328015366b9b8f *electron-v28.2.8-linux-x64-symbols.zip +e7d17ee311299dfef3d2916987a513c4c1b66ad2e417c15fa5d29699602bd6cb *electron-v28.2.8-linux-x64.zip +5f0179fd7bf3927381bde24c9fb372fe95328be0500918cd6ee7f9503fae1ef5 *electron-v28.2.8-mas-arm64-dsym-snapshot.zip +e9810019f1d7b1b5a93fd1aee8adda5a872ebfb170de6d55cdd55162b923432d *electron-v28.2.8-mas-arm64-dsym.zip +4781376244c7df89d119575e2788ad43fae4387d850ef672665688081b30997c *electron-v28.2.8-mas-arm64-symbols.zip +a3932199781970e0b2fdb805d6556287ca877b35ac19384da00474140e14c41f *electron-v28.2.8-mas-arm64.zip +326cde32079496e0d976c5b65e85e5ce208eea3d8d23cd92c9e25f0fa6b30f40 *electron-v28.2.8-mas-x64-dsym-snapshot.zip +59a2b3d28dba45ee3016f8ab49a71b0c55f99ef046476183bc36890c9d335a71 *electron-v28.2.8-mas-x64-dsym.zip +313ff88f568c39079a1b7a1011f77fa03890cb9bb53649a489643311303cc3b8 *electron-v28.2.8-mas-x64-symbols.zip +41ab9f3addea5066d7e0ace28ebaead7128a2073931473c847aa9133b7df9248 *electron-v28.2.8-mas-x64.zip +179de6dd4835216bcd3e8bb9eb4d4b54013df865f52dbf0d5214726fc31cba9a *electron-v28.2.8-win32-arm64-pdb.zip +8628dec571206001420c1d8655904883d5de7e772d51ab2101b002c22e0dd25c *electron-v28.2.8-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-arm64-toolchain-profile.zip +bb2a2a466d14c32c06ff09c42b3d1413f19fdc8a49a445d07d289fa453c268d3 *electron-v28.2.8-win32-arm64.zip +1d1efc3a1d17072bc76a4a63c8236a896d46f6f3badacd50bc5824149196d56f *electron-v28.2.8-win32-ia32-pdb.zip +9ddb1520de421a7c636160d01432c9bf111e6ef4b9a3be41b185c702c72353ac *electron-v28.2.8-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-ia32-toolchain-profile.zip +38e22f9b0a32e0fc26e81905214e244c0a5d5c19e13c8ca2329ac75b62881472 *electron-v28.2.8-win32-ia32.zip +8168296e0454377e0113a7d0f87535d3d0e0c1a8538e8079ee1aae9c7223bb02 *electron-v28.2.8-win32-x64-pdb.zip +a276e9e748fa7db970e7dcce6f4ae571d8615a44e5208c0fa3c03de08774a4aa *electron-v28.2.8-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-x64-toolchain-profile.zip +079cc98f7933992ac7154e21e160d4a4c6b3541c26b56fc6f8438e9eabc369b9 *electron-v28.2.8-win32-x64.zip +f838e4a7c24518c5fa25d4a23acf869737cfa88761019cea4f83ebfb302363ec *electron.d.ts +4450bcc66cece4ff2373563e0123799f95645fa155577a8f380211b29e8b4ec9 *ffmpeg-v28.2.8-darwin-arm64.zip +152e3ed53098d24f356d7ec640d19efc57f7f34c39d8b8278f2586985d4a99a1 *ffmpeg-v28.2.8-darwin-x64.zip +8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.8-linux-arm64.zip +51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.8-linux-armv7l.zip +acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.8-linux-x64.zip +15a2a4a28a66e65122eb4f2bd796ccd5b6ed45420a034878affd002fc8c290dc *ffmpeg-v28.2.8-mas-arm64.zip +2dfe2f524c5220f50c7b6fe08605a67631b5520e0c82842e1f41f677cac17643 *ffmpeg-v28.2.8-mas-x64.zip +313e2979f0df88715159c0737bfbb5ae1d5c79fb9820e94d2a93ba71d3324ecd *ffmpeg-v28.2.8-win32-arm64.zip +9e73bc07563aefa8b9625676939a410b35a823d961b96da0e8edd90d7e5fb47b *ffmpeg-v28.2.8-win32-ia32.zip +1b11042defc8a3f403e5567fa4a4b8c59b224f3b7b52d44d6c7197b96af7b53b *ffmpeg-v28.2.8-win32-x64.zip +1e2e9480d4228f6bbc731ff7ee413b9e97656c36b15418d20681a76d82902b86 *hunspell_dictionaries.zip +8c8b967cf4c78ed9bbf4921b2c616257f45b137412eb3bc64176066c3e47bbe8 *libcxx-objects-v28.2.8-linux-arm64.zip +56af259535ccfaac295b82ce68686f9582265cb2ebe2783852f518c0fabc8a1e *libcxx-objects-v28.2.8-linux-armv7l.zip +b590e001dc98e32e5952ca69573e6f1bcec5e2f2d99052d1089ab72084cccea1 *libcxx-objects-v28.2.8-linux-x64.zip +c0634d5c92f0a2983b17c866f7d3694cb75f6e78cd07b10d9488ef46acc66a50 *libcxx_headers.zip +99ee16441d9eb2b92a05d5a5c9b9dc4cdfab33cb09595e9d78fd2ba503dead5b *libcxxabi_headers.zip +a95de1da301d641caaafaea9869c4c7834c254f818ac0c10d97402b2220c8be3 *mksnapshot-v28.2.8-darwin-arm64.zip +e5ef6b35d7cd807f93babfedbbde513ab6053ad9fb80b0f7abc1bfda414daaa1 *mksnapshot-v28.2.8-darwin-x64.zip +eeb6c5b7962af8d5cfaa97b2cf96d312d0ad57a3abb3e00774d50ea2e005bb9b *mksnapshot-v28.2.8-linux-arm64-x64.zip +0adacd0767469f90400b1f17ba8ac3ccb33cfeb11a8ef54d70bc8adb7cc306dc *mksnapshot-v28.2.8-linux-armv7l-x64.zip +5242817f1f26e10804e7e2446d0a8a64e8b2958cdba01e79d89db883d9d960d0 *mksnapshot-v28.2.8-linux-x64.zip +0ecb67673508c10f4fe08e7cb80300b9a8f507f50994c79caf302ff78ef748ca *mksnapshot-v28.2.8-mas-arm64.zip +19429da56077f12de4d4563f49c55f4f1f0fe61f66863804640fc55e65ee98f9 *mksnapshot-v28.2.8-mas-x64.zip +c7b47ae63c2f6eb07b06379206e6f215fbcb2b9a49faa72ca850bf8f9b998c4c *mksnapshot-v28.2.8-win32-arm64-x64.zip +0032660a9f8575a153951f29adae49a18e400b40906eec803fe7e3d2e970503d *mksnapshot-v28.2.8-win32-ia32.zip +2c71c9a2bd4441e580dc3083073e712fba94e0236415c8ab35320da52f492508 *mksnapshot-v28.2.8-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 9ed8af5842a..13aa4c7e87b 100644 --- a/build/checksums/nodejs.txt +++ b/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/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index ded5a587e43..21c3f2b61fa 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -22,8 +22,6 @@ 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, @@ -31,81 +29,77 @@ const extensionsPath = path.join(path.dirname(__dirname), 'extensions'); // }); const compilations = [ // --- Start Positron --- - 'positron-code-cells/tsconfig.json', - 'positron-connections/tsconfig.json', - 'positron-javascript/tsconfig.json', - 'positron-notebook-controllers/tsconfig.json', - 'positron-notebooks/tsconfig.json', - 'positron-r/tsconfig.json', - 'positron-rstudio-keymap/tsconfig.json', - 'positron-python/tsconfig.json', - 'positron-proxy/tsconfig.json', - 'positron-viewer/tsconfig.json', - 'positron-zed/tsconfig.json', - // --- End Positron --- - 'authentication-proxy/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', - // --- Start Positron --- - 'jupyter-adapter/tsconfig.json', + 'extensions/positron-code-cells/tsconfig.json', + 'extensions/positron-connections/tsconfig.json', + 'extensions/positron-javascript/tsconfig.json', + 'extensions/positron-notebook-controllers/tsconfig.json', + 'extensions/positron-notebooks/tsconfig.json', + 'extensions/positron-r/tsconfig.json', + 'extensions/positron-rstudio-keymap/tsconfig.json', + 'extensions/positron-python/tsconfig.json', + 'extensions/positron-proxy/tsconfig.json', + 'extensions/positron-viewer/tsconfig.json', + 'extensions/positron-zed/tsconfig.json', + 'extensions/jupyter-adapter/tsconfig.json', // --- End Positron --- - '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/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; @@ -134,10 +128,10 @@ const tasks = compilations.map(function (tsconfigFile) { const input = es.through(); // --- Start Positron --- // Add '**/*.tsx'. - const tsFilter = filter(['**/*.ts', '**/*.tsx', '!**/lib/lib*.d.ts', '!**/node_modules/**'], { restore: true }); + const tsFilter = filter(['**/*.ts', '**/*.tsx', '!**/lib/lib*.d.ts', '!**/node_modules/**'], { restore: true, dot: true }); // Check if the extension has defined a bundle-dev task to be run let needsBundling = false; - const absoluteExtPath = path.join(extensionsPath, relativeDirname); + const absoluteExtPath = path.join(root, relativeDirname); const metadataPath = path.join(absoluteExtPath, 'package.json'); const webpackConfigPath = path.join(absoluteExtPath, 'extension.webpack.config.js'); @@ -174,7 +168,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 })) // --- Start Positron --- .pipe(needsBundling ? webpack({ @@ -357,6 +351,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/build/gulpfile.reh.js b/build/gulpfile.reh.js index f6ef5a03d70..766670ac2ef 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -385,7 +385,13 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa ); } - if (platform === 'linux' || platform === 'alpine') { + if (platform === 'linux' && process.env['VSCODE_NODE_GLIBC'] === '-glibc-2.17') { + result = es.merge(result, + gulp.src(`resources/server/bin/helpers/check-requirements-linux-legacy.sh`, { base: '.' }) + .pipe(rename(`bin/helpers/check-requirements.sh`)) + .pipe(util.setExecutableBit()) + ); + } else if (platform === 'linux' || platform === 'alpine') { result = es.merge(result, gulp.src(`resources/server/bin/helpers/check-requirements-linux.sh`, { base: '.' }) .pipe(rename(`bin/helpers/check-requirements.sh`)) diff --git a/build/lib/asar.js b/build/lib/asar.js index cadb9ab974d..31845f2f2dd 100644 --- a/build/lib/asar.js +++ b/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/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 1b0adc48d4c..463ce16e18d 100644 --- a/build/lib/builtInExtensions.js +++ b/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/build/lib/bundle.js b/build/lib/bundle.js index 5d3ee9d5118..61d9f015624 100644 --- a/build/lib/bundle.js +++ b/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/build/lib/compilation.js b/build/lib/compilation.js index 4956a93f6ef..a601c4f919b 100644 --- a/build/lib/compilation.js +++ b/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"); @@ -99,10 +102,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); @@ -140,7 +142,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); @@ -156,7 +157,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/build/lib/dependencies.js b/build/lib/dependencies.js index 64087a9ac17..1f2dd75d68c 100644 --- a/build/lib/dependencies.js +++ b/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/build/lib/extensions.js b/build/lib/extensions.js index 96344dbdb27..fdb8fa26ce0 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -4,7 +4,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.copyExtensionBinaries = 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; +exports.copyExtensionBinaries = copyExtensionBinaries; const es = require("event-stream"); const fs = require("fs"); const cp = require("child_process"); @@ -213,7 +221,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 +239,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 +312,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 +330,6 @@ function packageMarketplaceExtensionsStream(forWeb) { return (marketplaceExtensionsStream .pipe(util2.setExecutableBit(['**/*.sh']))); } -exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; function scanBuiltinExtensions(extensionsRoot, exclude = []) { const scannedExtensions = []; try { @@ -361,7 +365,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 +388,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 +461,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,7 +501,6 @@ async function buildExtensionMedia(isWatch, outputRoot) { outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined }))); } -exports.buildExtensionMedia = buildExtensionMedia; // --- Start Positron --- // This Gulp task is used to copy binaries verbatim from built-in extensions to // the output folder. VS Code's built-in extensions are webpacked, and weback @@ -558,6 +558,5 @@ async function copyExtensionBinaries(outputRoot) { resolve(); }); } -exports.copyExtensionBinaries = copyExtensionBinaries; // --- End Positron --- //# sourceMappingURL=extensions.js.map \ No newline at end of file diff --git a/build/lib/fetch.js b/build/lib/fetch.js index ba23e78257c..2fed63bca0e 100644 --- a/build/lib/fetch.js +++ b/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/build/lib/getVersion.js b/build/lib/getVersion.js index abf05e93210..b50ead538a2 100644 --- a/build/lib/getVersion.js +++ b/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/build/lib/git.js b/build/lib/git.js index a8e712ed070..798a408bdb9 100644 --- a/build/lib/git.js +++ b/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/build/lib/i18n.js b/build/lib/i18n.js index 1844af139c5..c33994987f0 100644 --- a/build/lib/i18n.js +++ b/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/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 1b5d44cb9e4..edc7b3fabba 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -418,6 +418,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" @@ -645,6 +649,10 @@ { "name": "vs/workbench/contrib/accountEntitlements", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/authentication", + "project": "vscode-workbench" } ] } diff --git a/build/lib/mangleTypeScript.js b/build/lib/mangleTypeScript.js deleted file mode 100644 index 45b50148d12..00000000000 --- a/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/build/lib/monaco-api.js b/build/lib/monaco-api.js index 6512b6ae886..2052806c46b 100644 --- a/build/lib/monaco-api.js +++ b/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/build/lib/nls.js b/build/lib/nls.js index 982f74bcf4d..48ca84f2433 100644 --- a/build/lib/nls.js +++ b/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/build/lib/optimize.js b/build/lib/optimize.js index 9dff0859acc..237f2bc20e8 100644 --- a/build/lib/optimize.js +++ b/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/build/lib/pandoc.js b/build/lib/pandoc.js index 52c90811899..82ca253d2cd 100644 --- a/build/lib/pandoc.js +++ b/build/lib/pandoc.js @@ -3,7 +3,8 @@ * Copyright (C) 2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getPandoc = exports.getPandocStream = void 0; +exports.getPandocStream = getPandocStream; +exports.getPandoc = getPandoc; const fancyLog = require("fancy-log"); const fetch_1 = require("./fetch"); const es = require("event-stream"); @@ -106,7 +107,6 @@ function getPandocStream() { getPandocMacOS(version) : getPandocLinux(version); } -exports.getPandocStream = getPandocStream; /** * Standalone helper for downloading and unpacking pandoc; downloads Pandoc to * thie `.build` folder for testing. @@ -123,7 +123,6 @@ function getPandoc() { .on('end', resolve); }); } -exports.getPandoc = getPandoc; if (require.main === module) { getPandoc().then(() => process.exit(0)).catch(err => { console.error(err); diff --git a/build/lib/reporter.js b/build/lib/reporter.js index 305d7364287..9d4a1b4fd79 100644 --- a/build/lib/reporter.js +++ b/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/build/lib/standalone.js b/build/lib/standalone.js index 4ddf88ed223..dbc47db0833 100644 --- a/build/lib/standalone.js +++ b/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/build/lib/stats.js b/build/lib/stats.js index d923bb809da..e089cb0c1b4 100644 --- a/build/lib/stats.js +++ b/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/build/lib/stylelint/validateVariableNames.js b/build/lib/stylelint/validateVariableNames.js index 2367fb94c2e..57b2aad957f 100644 --- a/build/lib/stylelint/validateVariableNames.js +++ b/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/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 308cf72b56e..6f8bcbccadf 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/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", @@ -696,6 +698,7 @@ "--vscode-sideBarSectionHeader-border", "--vscode-sideBarSectionHeader-foreground", "--vscode-sideBarTitle-foreground", + "--vscode-sideBarActivityBarTop-border", "--vscode-sideBySideEditor-horizontalBorder", "--vscode-sideBySideEditor-verticalBorder", "--vscode-simpleFindWidget-sashBorder", @@ -830,7 +833,6 @@ "--vscode-terminalOverviewRuler-findMatchForeground", "--vscode-terminalStickyScroll-background", "--vscode-terminalStickyScrollHover-background", - "--vscode-testing-coverage-lineHeight", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", "--vscode-testing-coveredBackground", @@ -855,6 +857,8 @@ "--vscode-testing-uncoveredBorder", "--vscode-testing-uncoveredBranchBackground", "--vscode-testing-uncoveredGutterBackground", + "--vscode-testing-uncoveredGutterBackground", + "--vscode-testing-coverage-lineHeight", "--vscode-textBlockQuote-background", "--vscode-textBlockQuote-border", "--vscode-textCodeBlock-background", @@ -886,8 +890,7 @@ "--vscode-widget-border", "--vscode-widget-shadow", "--vscode-window-activeBorder", - "--vscode-window-inactiveBorder", - "--vscode-multiDiffEditor-headerBackground" + "--vscode-window-inactiveBorder" ], "others": [ "--background-dark", @@ -928,8 +931,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/build/lib/task.js b/build/lib/task.js index 6b040a75698..597b2a0d397 100644 --- a/build/lib/task.js +++ b/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/build/lib/treeshaking.js b/build/lib/treeshaking.js index 51c610ecda2..c8e95511877 100644 --- a/build/lib/treeshaking.js +++ b/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/build/lib/tsb/builder.js b/build/lib/tsb/builder.js index e87945ea9cc..fc74bfa8acc 100644 --- a/build/lib/tsb/builder.js +++ b/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/build/lib/tsb/index.js b/build/lib/tsb/index.js index 47f26bc8178..8b8116d5a49 100644 --- a/build/lib/tsb/index.js +++ b/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/build/lib/util.js b/build/lib/util.js index 43c971475c0..d5a54916c58 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -4,7 +4,30 @@ * 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.stripImportStatements = 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.stripImportStatements = stripImportStatements; +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 +77,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 +105,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 +116,6 @@ function fixWin32DirectoryPermissions() { return f; }); } -exports.fixWin32DirectoryPermissions = fixWin32DirectoryPermissions; function setExecutableBit(pattern) { const setBit = es.mapSync(f => { if (!f.stat) { @@ -115,7 +135,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 +142,6 @@ function toFileUri(filePath) { } return 'file://' + filePath.replace(/\\/g, '/'); } -exports.toFileUri = toFileUri; function skipDirectories() { return es.mapSync(f => { if (!f.isDirectory()) { @@ -131,7 +149,6 @@ function skipDirectories() { } }); } -exports.skipDirectories = skipDirectories; function cleanNodeModules(rulePath) { const rules = fs.readFileSync(rulePath, 'utf8') .split(/\r?\n/g) @@ -143,7 +160,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 +201,6 @@ function loadSourcemaps() { })); return es.duplex(input, output); } -exports.loadSourcemaps = loadSourcemaps; function stripSourceMappingURL() { const input = es.through(); const output = input @@ -196,7 +211,6 @@ function stripSourceMappingURL() { })); return es.duplex(input, output); } -exports.stripSourceMappingURL = stripSourceMappingURL; // --- Start Positron --- /** * Strips and/or modifies import statements. This function only runs on @@ -240,7 +254,6 @@ function stripImportStatements() { })); return es.duplex(input, output); } -exports.stripImportStatements = stripImportStatements; // --- End Positron --- /** 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()) { @@ -249,7 +262,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(); @@ -263,7 +275,6 @@ function appendOwnPathSourceURL() { })); return es.duplex(input, output); } -exports.appendOwnPathSourceURL = appendOwnPathSourceURL; function rewriteSourceMappingURL(sourceMappingURLBase) { const input = es.through(); const output = input @@ -275,7 +286,6 @@ function rewriteSourceMappingURL(sourceMappingURLBase) { })); return es.duplex(input, output); } -exports.rewriteSourceMappingURL = rewriteSourceMappingURL; function rimraf(dir) { const result = () => new Promise((c, e) => { let retries = 0; @@ -295,7 +305,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) { @@ -312,7 +321,6 @@ function rreddir(dirPath) { _rreaddir(dirPath, '', result); return result; } -exports.rreddir = rreddir; function ensureDir(dirPath) { if (fs.existsSync(dirPath)) { return; @@ -320,14 +328,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)) { @@ -340,7 +346,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); @@ -349,21 +354,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'); @@ -422,7 +424,6 @@ function acquireWebNodePaths() { // --- End Positron --- return nodePaths; } -exports.acquireWebNodePaths = acquireWebNodePaths; function createExternalLoaderConfig(webEndpoint, commit, quality) { if (!webEndpoint || !commit || !quality) { return undefined; @@ -439,7 +440,6 @@ function createExternalLoaderConfig(webEndpoint, commit, quality) { }; return externalLoaderConfig; } -exports.createExternalLoaderConfig = createExternalLoaderConfig; function buildWebNodePaths(outDir) { const result = () => new Promise((resolve, _) => { const root = path.join(__dirname, '..', '..'); @@ -460,5 +460,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/build/linux/debian/calculate-deps.js b/build/linux/debian/calculate-deps.js index 6304df9edda..bbcb6bfc3de 100644 --- a/build/linux/debian/calculate-deps.js +++ b/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/build/linux/debian/install-sysroot.js b/build/linux/debian/install-sysroot.js index d637fce3ca6..feca7d3fa9d 100644 --- a/build/linux/debian/install-sysroot.js +++ b/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/build/linux/debian/types.js b/build/linux/debian/types.js index 2cd177c34a8..ce21d50e1a9 100644 --- a/build/linux/debian/types.js +++ b/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/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 04e5acafefe..776538b03e8 100644 --- a/build/linux/dependencies-generator.js +++ b/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"); @@ -31,7 +31,7 @@ const types_2 = require("./rpm/types"); // different from the machine used to build VSCode. const FAIL_BUILD_FOR_NEW_DEPENDENCIES = false; // --- End Positron -- -// 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 = [ @@ -103,7 +103,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/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 4d827c153de..1fc5688b80f 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -33,7 +33,7 @@ import { isRpmArchString, RpmArchString } from './rpm/types'; const FAIL_BUILD_FOR_NEW_DEPENDENCIES = false; // --- End Positron -- -// 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/build/linux/libcxx-fetcher.js b/build/linux/libcxx-fetcher.js index 1e195ba1fac..cfdc9498502 100644 --- a/build/linux/libcxx-fetcher.js +++ b/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/build/linux/rpm/calculate-deps.js b/build/linux/rpm/calculate-deps.js index ac870e4a546..b19e26f1854 100644 --- a/build/linux/rpm/calculate-deps.js +++ b/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/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index b9a6e80d5f3..bd84fc146dc 100644 --- a/build/linux/rpm/dep-lists.js +++ b/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/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 275d88b95a8..82a4fe7698d 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/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/build/linux/rpm/types.js b/build/linux/rpm/types.js index 6dba7cf38d1..a20b9c2fe02 100644 --- a/build/linux/rpm/types.js +++ b/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/build/win32/explorer-appx-fetcher.js b/build/win32/explorer-appx-fetcher.js index d618c21674a..554b449d872 100644 --- a/build/win32/explorer-appx-fetcher.js +++ b/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/cglicenses.json b/cglicenses.json index 1d6851e38c8..6ca8ba33b4d 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -593,11 +593,11 @@ { // Reason: mono-repo where the individual packages are also dual-licensed under MIT and Apache-2.0 "name": "system-configuration", - "fullLicenseTextUri": "https://github.com/mullvad/system-configuration-rs/blob/main/system-configuration/LICENSE-MIT" + "fullLicenseTextUri": "https://raw.githubusercontent.com/mullvad/system-configuration-rs/v0.6.0/system-configuration/LICENSE-MIT" }, { // Reason: mono-repo where the individual packages are also dual-licensed under MIT and Apache-2.0 "name": "system-configuration-sys", - "fullLicenseTextUri": "https://github.com/mullvad/system-configuration-rs/blob/main/system-configuration-sys/LICENSE-MIT" + "fullLicenseTextUri": "https://raw.githubusercontent.com/mullvad/system-configuration-rs/v0.6.0/system-configuration-sys/LICENSE-MIT" } ] diff --git a/cgmanifest.json b/cgmanifest.json index 2673931fdb6..891a0b0cb32 100644 --- a/cgmanifest.json +++ b/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": "31cd9d1f61714e20f1067d726404600ab7281698" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "27.3.2" + "version": "28.2.8" }, { "component": { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 553df3f8f53..4be3e46eb7f 100644 --- a/cli/Cargo.lock +++ b/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/cli/Cargo.toml b/cli/Cargo.toml index f51f31e9fb5..db058cd9f7c 100644 --- a/cli/Cargo.toml +++ b/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/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 6ec82c8b541..f65f8a33397 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -3827,7 +3827,7 @@ DEALINGS IN THE SOFTWARE. indexmap 1.9.1 - Apache-2.0 OR MIT indexmap 2.1.0 - Apache-2.0 OR MIT -https://github.com/bluss/indexmap +https://github.com/indexmap-rs/indexmap Copyright (c) 2016--2017 @@ -4232,7 +4232,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -libc 0.2.144 - MIT OR Apache-2.0 +libc 0.2.153 - MIT OR Apache-2.0 https://github.com/rust-lang/libc Copyright (c) 2014-2020 The Rust Project Developers @@ -4597,7 +4597,7 @@ The parts of miniz that are not covered by the unlicense is [some Zip64 code](ht --------------------------------------------------------- -mio 0.8.4 - MIT +mio 0.8.11 - MIT https://github.com/tokio-rs/mio The MIT License (MIT) @@ -6189,6 +6189,34 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +os_info 3.7.0 - MIT +https://github.com/stanislav-tkach/os_info + +The MIT License (MIT) + +Copyright (c) 2017 Stanislav Tkach + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + parking 2.0.0 - Apache-2.0 OR MIT https://github.com/smol-rs/parking @@ -8637,7 +8665,7 @@ SOFTWARE. system-configuration 0.5.1 - MIT OR Apache-2.0 https://github.com/mullvad/system-configuration-rs -Copyright (c) 2017 Amagicom AB +Copyright (c) 2024 Mullvad VPN AB Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -8669,7 +8697,7 @@ DEALINGS IN THE SOFTWARE. system-configuration-sys 0.5.0 - MIT OR Apache-2.0 https://github.com/mullvad/system-configuration-rs -Copyright (c) 2017 Amagicom AB +Copyright (c) 2024 Mullvad VPN AB Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -8853,7 +8881,7 @@ DEALINGS IN THE SOFTWARE. time 0.3.21 - MIT OR Apache-2.0 https://github.com/time-rs/time -Copyright (c) 2022 Jacob Pratt et al. +Copyright (c) 2024 Jacob Pratt et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -8879,7 +8907,7 @@ SOFTWARE. time-core 0.1.1 - MIT OR Apache-2.0 https://github.com/time-rs/time -Copyright (c) 2022 Jacob Pratt et al. +Copyright (c) 2024 Jacob Pratt et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9359,7 +9387,7 @@ THE SOFTWARE. --------------------------------------------------------- -tunnels 97233d20448e1c3cb0e0fd9114acf68c7e5c0249 +tunnels 8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30 https://github.com/microsoft/dev-tunnels MIT License @@ -9613,7 +9641,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -urlencoding 2.1.2 - MIT +urlencoding 2.1.3 - MIT https://github.com/kornelski/rust_urlencoding The MIT License (MIT) @@ -10595,6 +10623,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- winreg 0.50.0 - MIT +winreg 0.8.0 - MIT https://github.com/gentoo90/winreg-rs The MIT License (MIT) diff --git a/cli/src/auth.rs b/cli/src/auth.rs index ee7117330be..9d5c9b73fdb 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -60,7 +60,7 @@ impl Display for AuthProvider { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AuthProvider::Microsoft => write!(f, "Microsoft Account"), - AuthProvider::Github => write!(f, "Github Account"), + AuthProvider::Github => write!(f, "GitHub Account"), } } } @@ -144,7 +144,7 @@ impl StoredCredential { let res = match res { Ok(r) => r, Err(e) => { - warning!(log, "failed to check Github token: {}", e); + warning!(log, "failed to check GitHub token: {}", e); return false; } }; @@ -154,7 +154,7 @@ impl StoredCredential { } let err = StatusError::from_res(res).await; - debug!(log, "github token looks expired: {:?}", err); + debug!(log, "GitHub token looks expired: {:?}", err); true } } @@ -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() @@ -675,7 +678,7 @@ impl Auth { if !*IS_INTERACTIVE_CLI { info!( self.log, - "Using Github for authentication, run `{} tunnel user login --provider ` option to change this.", + "Using GitHub for authentication, run `{} tunnel user login --provider ` option to change this.", APPLICATION_NAME ); return Ok(AuthProvider::Github); diff --git a/cli/src/bin/code/legacy_args.rs b/cli/src/bin/code/legacy_args.rs index 3f134443641..0bd92c92fd3 100644 --- a/cli/src/bin/code/legacy_args.rs +++ b/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/cli/src/commands.rs b/cli/src/commands.rs index d10a52ad774..027716947a3 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -6,8 +6,8 @@ mod context; pub mod args; +pub mod serve_web; pub mod tunnels; pub mod update; pub mod version; -pub mod serve_web; pub use context::CommandContext; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 229d54ad061..dcdbd808fe7 100644 --- a/cli/src/commands/args.rs +++ b/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,33 @@ 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, + + /// Specifies the directory that server data is kept in. + #[clap(long)] + pub server_data_dir: Option, + + /// Set the root path for extensions. + #[clap(long)] + pub extensions_dir: Option, +} + +impl TunnelServeArgs { + pub fn apply_to_server_args(&self, csa: &mut CodeServerArgs) { + csa.install_extensions + .extend_from_slice(&self.install_extension); + + if let Some(d) = &self.server_data_dir { + csa.server_data_dir = Some(d.clone()); + } + + if let Some(d) = &self.extensions_dir { + csa.extensions_dir = Some(d.clone()); + } + } } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 959763a431d..fba92723426 100644 --- a/cli/src/commands/serve_web.rs +++ b/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/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index f9ae6883075..59c5794bd93 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -53,6 +53,7 @@ use crate::{ app_lock::AppMutex, command::new_std_command, errors::{wrap, AnyError, CodeError}, + machine::canonical_exe, prereqs::PreReqChecker, }, }; @@ -230,8 +231,7 @@ pub async fn service( // likewise for license consent legal::require_consent(&ctx.paths, args.accept_server_license_terms)?; - let current_exe = - std::env::current_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( @@ -407,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/cli/src/state.rs b/cli/src/state.rs index 8815e2df40c..534c1556763 100644 --- a/cli/src/state.rs +++ b/cli/src/state.rs @@ -6,7 +6,8 @@ extern crate dirs; use std::{ - fs::{create_dir_all, read_to_string, remove_dir_all, write}, + fs::{self, create_dir_all, read_to_string, remove_dir_all}, + io::Write, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; @@ -34,6 +35,8 @@ where { path: PathBuf, state: Option, + #[allow(dead_code)] + mode: u32, } impl PersistedStateContainer @@ -58,13 +61,28 @@ where fn save(&mut self, state: T) -> Result<(), WrappedError> { let s = serde_json::to_string(&state).unwrap(); self.state = Some(state); - write(&self.path, s).map_err(|e| { + self.write_state(s).map_err(|e| { wrap( e, format!("error saving launcher state into {}", self.path.display()), ) }) } + + fn write_state(&mut self, s: String) -> std::io::Result<()> { + #[cfg(not(windows))] + use std::os::unix::fs::OpenOptionsExt; + + let mut f = fs::OpenOptions::new(); + f.create(true); + f.write(true); + f.truncate(true); + #[cfg(not(windows))] + f.mode(self.mode); + + let mut f = f.open(&self.path)?; + f.write_all(s.as_bytes()) + } } /// Container that holds some state value that is persisted to disk. @@ -82,8 +100,17 @@ where { /// Creates a new state container that persists to the given path. pub fn new(path: PathBuf) -> PersistedState { + Self::new_with_mode(path, 0o644) + } + + /// Creates a new state container that persists to the given path. + pub fn new_with_mode(path: PathBuf, mode: u32) -> PersistedState { PersistedState { - container: Arc::new(Mutex::new(PersistedStateContainer { path, state: None })), + container: Arc::new(Mutex::new(PersistedStateContainer { + path, + state: None, + mode, + })), } } @@ -217,5 +244,4 @@ impl LauncherPaths { pub fn web_server_storage(&self) -> PathBuf { self.root.join("serve-web") } - } diff --git a/cli/src/tunnels.rs b/cli/src/tunnels.rs index 7378cf34afd..452da4dc3e9 100644 --- a/cli/src/tunnels.rs +++ b/cli/src/tunnels.rs @@ -6,14 +6,13 @@ pub mod code_server; pub mod dev_tunnels; pub mod legal; +pub mod local_forwarding; pub mod paths; pub mod protocol; pub mod shutdown_signal; pub mod singleton_client; pub mod singleton_server; -pub mod local_forwarding; -mod wsl_detect; mod challenge; mod control_server; mod nosleep; @@ -34,8 +33,9 @@ mod service_macos; #[cfg(target_os = "windows")] mod service_windows; mod socket_signal; +mod wsl_detect; -pub use control_server::{serve, serve_stream, Next, ServeStreamParams, AuthRequired}; +pub use control_server::{serve, serve_stream, AuthRequired, Next, ServeStreamParams}; pub use nosleep::SleepInhibitor; pub use service::{ create_service_manager, ServiceContainer, ServiceManager, SERVICE_LOG_FILE_NAME, diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index bb854001d54..bb5a9f8d08f 100644 --- a/cli/src/tunnels/code_server.rs +++ b/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}; @@ -56,6 +57,8 @@ pub struct CodeServerArgs { pub log: Option, pub accept_server_license_terms: bool, pub verbose: bool, + pub server_data_dir: Option, + pub extensions_dir: Option, // extension management pub install_extensions: Vec, pub uninstall_extensions: Vec, @@ -143,6 +146,12 @@ impl CodeServerArgs { args.push(format!("--category={}", i)); } } + if let Some(d) = &self.server_data_dir { + args.push(format!("--server-data-dir={}", d)); + } + if let Some(d) = &self.extensions_dir { + args.push(format!("--extensions-dir={}", d)); + } if self.start_server { args.push(String::from("--start-server")); } @@ -488,6 +497,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/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index 5f564494b98..9aae5ef3f07 100644 --- a/cli/src/tunnels/control_server.rs +++ b/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/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index 94396e89977..3bf8a331e74 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/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/cli/src/tunnels/service_linux.rs b/cli/src/tunnels/service_linux.rs index b60d114dc46..80599ba3c32 100644 --- a/cli/src/tunnels/service_linux.rs +++ b/cli/src/tunnels/service_linux.rs @@ -90,6 +90,10 @@ impl ServiceManager for SystemdService { info!(self.log, "Successfully registered service..."); + if let Err(e) = proxy.reload().await { + warning!(self.log, "Error issuing reload(): {}", e); + } + // note: enablement is implicit in recent systemd version, but required for older systems // https://github.com/microsoft/vscode/issues/167489#issuecomment-1331222826 proxy @@ -257,4 +261,7 @@ trait SystemdManagerDbus { #[dbus_proxy(name = "StopUnit")] fn stop_unit(&self, name: String, mode: String) -> zbus::Result; + + #[dbus_proxy(name = "Reload")] + fn reload(&self) -> zbus::Result<()>; } diff --git a/cli/src/tunnels/socket_signal.rs b/cli/src/tunnels/socket_signal.rs index 9036c6ae3f9..2227f323852 100644 --- a/cli/src/tunnels/socket_signal.rs +++ b/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/cli/src/update_service.rs b/cli/src/update_service.rs index d218e4a1333..4bec13d6e86 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -209,8 +209,11 @@ pub enum Platform { LinuxAlpineX64, LinuxAlpineARM64, LinuxX64, + LinuxX64Legacy, LinuxARM64, + LinuxARM64Legacy, LinuxARM32, + LinuxARM32Legacy, DarwinX64, DarwinARM64, WindowsX64, @@ -237,8 +240,11 @@ impl Platform { Platform::LinuxAlpineARM64 => "server-alpine-arm64", Platform::LinuxAlpineX64 => "server-linux-alpine", Platform::LinuxX64 => "server-linux-x64", + Platform::LinuxX64Legacy => "server-linux-legacy-x64", Platform::LinuxARM64 => "server-linux-arm64", + Platform::LinuxARM64Legacy => "server-linux-legacy-arm64", Platform::LinuxARM32 => "server-linux-armhf", + Platform::LinuxARM32Legacy => "server-linux-legacy-armhf", Platform::DarwinX64 => "server-darwin", Platform::DarwinARM64 => "server-darwin-arm64", Platform::WindowsX64 => "server-win32-x64", @@ -253,8 +259,11 @@ impl Platform { Platform::LinuxAlpineARM64 => "cli-alpine-arm64", Platform::LinuxAlpineX64 => "cli-alpine-x64", Platform::LinuxX64 => "cli-linux-x64", + Platform::LinuxX64Legacy => "cli-linux-x64", Platform::LinuxARM64 => "cli-linux-arm64", + Platform::LinuxARM64Legacy => "cli-linux-arm64", Platform::LinuxARM32 => "cli-linux-armhf", + Platform::LinuxARM32Legacy => "cli-linux-armhf", Platform::DarwinX64 => "cli-darwin-x64", Platform::DarwinARM64 => "cli-darwin-arm64", Platform::WindowsARM64 => "cli-win32-arm64", @@ -309,8 +318,11 @@ impl fmt::Display for Platform { Platform::LinuxAlpineARM64 => "LinuxAlpineARM64", Platform::LinuxAlpineX64 => "LinuxAlpineX64", Platform::LinuxX64 => "LinuxX64", + Platform::LinuxX64Legacy => "LinuxX64Legacy", Platform::LinuxARM64 => "LinuxARM64", + Platform::LinuxARM64Legacy => "LinuxARM64Legacy", Platform::LinuxARM32 => "LinuxARM32", + Platform::LinuxARM32Legacy => "LinuxARM32Legacy", Platform::DarwinX64 => "DarwinX64", Platform::DarwinARM64 => "DarwinARM64", Platform::WindowsX64 => "WindowsX64", diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index 03280d12f0a..fc706199aab 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -471,7 +471,7 @@ pub enum CodeError { #[error("platform not currently supported: {0}")] UnsupportedPlatform(String), - #[error("This machine does not meet {name}'s prerequisites, expected either...: {bullets}")] + #[error("This machine does not meet {name}'s prerequisites, expected either...\n{bullets}")] PrerequisitesFailed { name: &'static str, bullets: String }, #[error("failed to spawn process: {0:?}")] ProcessSpawnFailed(std::io::Error), @@ -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/cli/src/util/io.rs b/cli/src/util/io.rs index 95b378c0c65..93a7efbfdd9 100644 --- a/cli/src/util/io.rs +++ b/cli/src/util/io.rs @@ -241,11 +241,7 @@ mod tests { let mut rx = tailf(read_file, 32); assert!(rx.try_recv().is_err()); - let mut append_file = OpenOptions::new() - .write(true) - .append(true) - .open(&file_path) - .unwrap(); + let mut append_file = OpenOptions::new().append(true).open(&file_path).unwrap(); writeln!(&mut append_file, "some line").unwrap(); let recv = rx.recv().await; @@ -338,11 +334,7 @@ mod tests { assert!(rx.try_recv().is_err()); - let mut append_file = OpenOptions::new() - .write(true) - .append(true) - .open(&file_path) - .unwrap(); + let mut append_file = OpenOptions::new().append(true).open(&file_path).unwrap(); writeln!(append_file, " is now complete").unwrap(); let recv = rx.recv().await; diff --git a/cli/src/util/machine.rs b/cli/src/util/machine.rs index 4c7b6729e43..1eb0759f9f9 100644 --- a/cli/src/util/machine.rs +++ b/cli/src/util/machine.rs @@ -3,7 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use std::{path::Path, time::Duration}; +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + time::Duration, +}; use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; pub fn process_at_path_exists(pid: u32, name: &Path) -> bool { @@ -71,3 +75,78 @@ pub async fn wait_until_exe_deleted(current_exe: &Path, poll_ms: u64) { tokio::time::sleep(duration).await; } } + +/// Gets the canonical current exe location, referring to the "current" symlink +/// if running inside snap. +pub fn canonical_exe() -> std::io::Result { + canonical_exe_inner( + std::env::current_exe(), + std::env::var_os("SNAP"), + std::env::var_os("SNAP_REVISION"), + ) +} + +#[inline(always)] +#[allow(unused_variables)] +fn canonical_exe_inner( + exe: std::io::Result, + snap: Option, + rev: Option, +) -> std::io::Result { + let exe = exe?; + + #[cfg(target_os = "linux")] + if let (Some(snap), Some(rev)) = (snap, rev) { + if !exe.starts_with(snap) { + return Ok(exe); + } + + let mut out = PathBuf::new(); + for part in exe.iter() { + if part == rev { + out.push("current") + } else { + out.push(part) + } + } + + return Ok(out); + } + + Ok(exe) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + #[cfg(target_os = "linux")] + fn test_canonical_exe_in_snap() { + let exe = canonical_exe_inner( + Ok(PathBuf::from("/snap/my-snap/1234/some/exe")), + Some("/snap/my-snap/1234".into()), + Some("1234".into()), + ) + .unwrap(); + assert_eq!(exe, PathBuf::from("/snap/my-snap/current/some/exe")); + } + + #[test] + fn test_canonical_exe_not_in_snap() { + let exe = canonical_exe_inner( + Ok(PathBuf::from("/not-in-snap")), + Some("/snap/my-snap/1234".into()), + Some("1234".into()), + ) + .unwrap(); + assert_eq!(exe, PathBuf::from("/not-in-snap")); + } + + #[test] + fn test_canonical_exe_not_in_snap2() { + let exe = canonical_exe_inner(Ok(PathBuf::from("/not-in-snap")), None, None).unwrap(); + assert_eq!(exe, PathBuf::from("/not-in-snap")); + } +} diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index b22fd469fac..20a5bc94b37 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ use std::cmp::Ordering; -use super::command::capture_command; use crate::constants::QUALITYLESS_SERVER_NAME; use crate::update_service::Platform; use lazy_static::lazy_static; @@ -20,8 +19,10 @@ lazy_static! { static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap(); static ref LIBSTD_CXX_VERSION_RE: BinRegex = BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); - static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); - static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0); + static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); + static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); + static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 28, 0); + static ref MIN_LEGACY_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0); } const NIXOS_TEST_PATH: &str = "/etc/NIXOS"; @@ -63,18 +64,30 @@ impl PreReqChecker { } else { println!("!!! WARNING: Skipping server pre-requisite check !!!"); println!("!!! Server stability is not guaranteed. Proceed at your own risk. !!!"); - (Ok(()), Ok(())) + (Ok(false), Ok(false)) }; - if (gnu_a.is_ok() && gnu_b.is_ok()) || is_nixos { - return Ok(if cfg!(target_arch = "x86_64") { - Platform::LinuxX64 - } else if cfg!(target_arch = "arm") { - Platform::LinuxARM32 - } else { - Platform::LinuxARM64 - }); - } + match (&gnu_a, &gnu_b, is_nixos) { + (Ok(false), Ok(false), _) | (_, _, true) => { + return Ok(if cfg!(target_arch = "x86_64") { + Platform::LinuxX64 + } else if cfg!(target_arch = "arm") { + Platform::LinuxARM32 + } else { + Platform::LinuxARM64 + }); + } + (Ok(_), Ok(_), _) => { + return Ok(if cfg!(target_arch = "x86_64") { + Platform::LinuxX64Legacy + } else if cfg!(target_arch = "arm") { + Platform::LinuxARM32Legacy + } else { + Platform::LinuxARM64Legacy + }); + } + _ => {} + }; if or_musl.is_ok() { return Ok(if cfg!(target_arch = "x86_64") { @@ -126,8 +139,9 @@ async fn check_musl_interpreter() -> Result<(), String> { Ok(()) } -#[allow(dead_code)] -async fn check_glibc_version() -> Result<(), String> { +/// Checks the glibc version, returns "true" if the legacy server is required. +#[cfg(target_os = "linux")] +async fn check_glibc_version() -> Result { #[cfg(target_env = "gnu")] let version = { let v = unsafe { libc::gnu_get_libc_version() }; @@ -137,7 +151,7 @@ async fn check_glibc_version() -> Result<(), String> { }; #[cfg(not(target_env = "gnu"))] let version = { - capture_command("ldd", ["--version"]) + super::command::capture_command("ldd", ["--version"]) .await .ok() .and_then(|o| extract_ldd_version(&o.stdout)) @@ -145,7 +159,9 @@ async fn check_glibc_version() -> Result<(), String> { if let Some(v) = version { return if v >= *MIN_LDD_VERSION { - Ok(()) + Ok(false) + } else if v >= *MIN_LEGACY_LDD_VERSION { + Ok(true) } else { Err(format!( "find GLIBC >= {} (but found {} instead) for GNU environments", @@ -154,7 +170,7 @@ async fn check_glibc_version() -> Result<(), String> { }; } - Ok(()) + Ok(false) } /// Check for nixos to avoid mandating glibc versions. See: @@ -180,8 +196,9 @@ pub async fn skip_requirements_check() -> bool { false } -#[allow(dead_code)] -async fn check_glibcxx_version() -> Result<(), String> { +/// Checks the glibc++ version, returns "true" if the legacy server is required. +#[cfg(target_os = "linux")] +async fn check_glibcxx_version() -> Result { let mut libstdc_path: Option = None; #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] @@ -193,7 +210,7 @@ async fn check_glibcxx_version() -> Result<(), String> { if fs::metadata(DEFAULT_LIB_PATH).await.is_ok() { libstdc_path = Some(DEFAULT_LIB_PATH.to_owned()); } else if fs::metadata(LDCONFIG_PATH).await.is_ok() { - libstdc_path = capture_command(LDCONFIG_PATH, ["-p"]) + libstdc_path = super::command::capture_command(LDCONFIG_PATH, ["-p"]) .await .ok() .and_then(|o| extract_libstd_from_ldconfig(&o.stdout)); @@ -211,30 +228,35 @@ async fn check_glibcxx_version() -> Result<(), String> { } } -#[allow(dead_code)] -fn check_for_sufficient_glibcxx_versions(contents: Vec) -> Result<(), String> { - let all_versions: Vec = LIBSTD_CXX_VERSION_RE +#[cfg(target_os = "linux")] +fn check_for_sufficient_glibcxx_versions(contents: Vec) -> Result { + let max_version = LIBSTD_CXX_VERSION_RE .captures_iter(&contents) .map(|m| SimpleSemver { major: m.get(1).map_or(0, |s| u32_from_bytes(s.as_bytes())), minor: m.get(2).map_or(0, |s| u32_from_bytes(s.as_bytes())), patch: m.get(3).map_or(0, |s| u32_from_bytes(s.as_bytes())), }) - .collect(); + .max(); - if !all_versions.iter().any(|v| &*MIN_CXX_VERSION >= v) { - return Err(format!( - "find GLIBCXX >= {} (but found {} instead) for GNU environments", - *MIN_CXX_VERSION, - all_versions - .iter() - .map(String::from) - .collect::>() - .join(", ") - )); + if let Some(max_version) = &max_version { + if max_version >= &*MIN_CXX_VERSION { + return Ok(false); + } + + if max_version >= &*MIN_LEGACY_CXX_VERSION { + return Ok(true); + } } - Ok(()) + Err(format!( + "find GLIBCXX >= {} (but found {} instead) for GNU environments", + *MIN_CXX_VERSION, + max_version + .as_ref() + .map(String::from) + .unwrap_or("none".to_string()) + )) } #[allow(dead_code)] @@ -255,6 +277,7 @@ fn extract_generic_version(output: &str) -> Option { }) } +#[allow(dead_code)] fn extract_libstd_from_ldconfig(output: &[u8]) -> Option { String::from_utf8_lossy(output) .lines() @@ -326,12 +349,12 @@ mod tests { #[test] fn test_extract_libstd_from_ldconfig() { let actual = " - libstoken.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstoken.so.1 - libstemmer.so.0d (libc6,x86-64) => /lib/x86_64-linux-gnu/libstemmer.so.0d - libstdc++.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstdc++.so.6 - libstartup-notification-1.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstartup-notification-1.so.0 - libssl3.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libssl3.so - ".to_owned().into_bytes(); + libstoken.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstoken.so.1 + libstemmer.so.0d (libc6,x86-64) => /lib/x86_64-linux-gnu/libstemmer.so.0d + libstdc++.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstdc++.so.6 + libstartup-notification-1.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstartup-notification-1.so.0 + libssl3.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libssl3.so + ".to_owned().into_bytes(); assert_eq!( extract_libstd_from_ldconfig(&actual), @@ -358,10 +381,10 @@ mod tests { #[test] fn check_for_sufficient_glibcxx_versions() { let actual = "ldd (Ubuntu GLIBC 2.31-0ubuntu9.7) 2.31 - Copyright (C) 2020 Free Software Foundation, Inc. - This is free software; see the source for copying conditions. There is NO - warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - Written by Roland McGrath and Ulrich Drepper." + Copyright (C) 2020 Free Software Foundation, Inc. + This is free software; see the source for copying conditions. There is NO + warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + Written by Roland McGrath and Ulrich Drepper." .to_owned() .into_bytes(); diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index bbf77e6017e..6135df5315a 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -36,6 +36,11 @@ export class SettingsDocument { return this.provideLanguageCompletionItems(location, position); } + // workbench.editor.label + if (location.path[0] === 'workbench.editor.label.patterns') { + return this.provideEditorLabelCompletionItems(location, position); + } + // settingsSync.ignoredExtensions if (location.path[0] === 'settingsSync.ignoredExtensions') { let ignoredExtensions = []; @@ -126,6 +131,31 @@ export class SettingsDocument { return completions; } + private async provideEditorLabelCompletionItems(location: Location, pos: vscode.Position): Promise { + const completions: vscode.CompletionItem[] = []; + + if (!this.isCompletingPropertyValue(location, pos)) { + return completions; + } + + let range = this.document.getWordRangeAtPosition(pos, /\$\{[^"\}]*\}?/); + if (!range || range.start.isEqual(pos) || range.end.isEqual(pos) && this.document.getText(range).endsWith('}')) { + range = new vscode.Range(pos, pos); + } + + const getText = (variable: string) => { + const text = '${' + variable + '}'; + return location.previousNode ? text : JSON.stringify(text); + }; + + + completions.push(this.newSimpleCompletionItem(getText('dirname'), range, vscode.l10n.t("The parent folder name of the editor (e.g. myFileFolder)"))); + completions.push(this.newSimpleCompletionItem(getText('dirname(1)'), range, vscode.l10n.t("The nth parent folder name of the editor"))); + completions.push(this.newSimpleCompletionItem(getText('filename'), range, vscode.l10n.t("The file name of the editor without its directory or extension (e.g. myFile)"))); + completions.push(this.newSimpleCompletionItem(getText('extname'), range, vscode.l10n.t("The file extension of the editor (e.g. txt)"))); + return completions; + } + private async provideFilesAssociationsCompletionItems(location: Location, position: vscode.Position): Promise { const completions: vscode.CompletionItem[] = []; diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 06e58ee064d..e105c0090ee 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -994,7 +994,7 @@ ] }, "dependencies": { - "vscode-languageclient": "9.0.1", + "vscode-languageclient": "^9.0.1", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index b5ff75c7686..0a790771db2 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -11,8 +11,8 @@ "browser": "./dist/browser/cssServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.12", - "vscode-languageserver": "^9.0.2-next.1", + "vscode-css-languageservice": "^6.2.13", + "vscode-languageserver": "^10.0.0-next.2", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 2e2a0c9a6ca..b7f86c89a11 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -24,28 +24,28 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-css-languageservice@^6.2.12: - version "6.2.12" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.12.tgz#f8f9f335fb4b433f557c51c62e687b4f62c0c786" - integrity sha512-PS9r7HgNjqzRl3v91sXpCyZPc8UDotNo6gntFNtGCKPhGA9Frk7g/VjX1Mbv3F00pn56D+rxrFzR9ep4cawOgA== +vscode-css-languageservice@^6.2.13: + version "6.2.13" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz#c7c2dc7a081a203048d60157c65536767d6d96f8" + integrity sha512-2rKWXfH++Kxd9Z4QuEgd1IF7WmblWWU7DScuyf1YumoGLkY9DW6wF/OTlhOyO2rN63sWHX2dehIpKBbho4ZwvA== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@8.2.1-next.1: - version "8.2.1-next.1" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1-next.1.tgz#52e1091907b56759114fabac803b18c44a48f2a9" - integrity sha512-L+DYtdUtqUXGpyMgHqer6IBKvFFhl/1ToiMmCmG85LYHuuX0jllHMz77MYt0RicakoYY+Lq1yLK6Qj3YBqgzDQ== +vscode-jsonrpc@9.0.0-next.2: + version "9.0.0-next.2" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" + integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== -vscode-languageserver-protocol@3.17.6-next.1: - version "3.17.6-next.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.1.tgz#5d87f7f708667cf04dbefb5c860901df7d01ebc1" - integrity sha512-2npXUc8oe/fb9Bjcwm2HTWYZXyCbW4NTo7jkOrEciGO+/LfWbSMgqZ6PwKWgqUkgCbkPxQHNjoMqr9ol/Ehjgg== +vscode-languageserver-protocol@3.17.6-next.3: + version "3.17.6-next.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.3.tgz#09d3e28e9ad12270233d07fa0b69cf1d51d7dfe4" + integrity sha512-H8ATH5SAvc3JzttS+AL6g681PiBOZM/l34WP2JZk4akY3y7NqTP+f9cJ+MhrVBbD3aDS8bdAKewZgbFLW6M8Pg== dependencies: - vscode-jsonrpc "8.2.1-next.1" - vscode-languageserver-types "3.17.6-next.1" + vscode-jsonrpc "9.0.0-next.2" + vscode-languageserver-types "3.17.6-next.3" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -57,17 +57,17 @@ vscode-languageserver-types@3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.1: - version "3.17.6-next.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.1.tgz#a3d2006d52f7d4026ea67668113ec16c73cd8f1d" - integrity sha512-7xVc/xLtNhKuCKX0mINT6mFUrUuRz0EinhwPGT8Gtsv2hlo+xJb5NKbiGailcWa1/T5e4dr5Pb2MfGchHreHAA== +vscode-languageserver-types@3.17.6-next.3: + version "3.17.6-next.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" + integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== -vscode-languageserver@^9.0.2-next.1: - version "9.0.2-next.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.2-next.1.tgz#cc9bbd66716346aa761e5bafa19d64559ab4e030" - integrity sha512-xySldxoHIcKXtxoI0LqRX3QcTdOVFt1SeHV0hyPq28p7xGPqWxUPcmTcfIqYdHefXG22nd8DQbGWOEe52yu08A== +vscode-languageserver@^10.0.0-next.2: + version "10.0.0-next.2" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.2.tgz#9a8ac58f72979961497c4fd7f6097561d4134d5f" + integrity sha512-WZdK/XO6EkNU6foYck49NpS35sahWhYFs4hwCGalH/6lhPmdUKABTnWioK/RLZKWqH8E5HdlAHQMfSBIxKBV9Q== dependencies: - vscode-languageserver-protocol "3.17.6-next.1" + vscode-languageserver-protocol "3.17.6-next.3" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 794ee50acbe..d01914101ca 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -4,50 +4,50 @@ "@types/node@18.x": version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + resolved "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz" integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== brace-expansion@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: balanced-match "^1.0.0" lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" minimatch@^5.1.0: version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" semver@^7.3.7: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + version "7.6.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" vscode-jsonrpc@8.2.0: version "8.2.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz" integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== -vscode-languageclient@9.0.1: +vscode-languageclient@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz#cdfe20267726c8d4db839dc1e9d1816e1296e854" + resolved "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz" integrity sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA== dependencies: minimatch "^5.1.0" @@ -56,7 +56,7 @@ vscode-languageclient@9.0.1: vscode-languageserver-protocol@3.17.5: version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + resolved "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz" integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== dependencies: vscode-jsonrpc "8.2.0" @@ -64,15 +64,15 @@ vscode-languageserver-protocol@3.17.5: 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" + resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== vscode-uri@^3.0.8: version "3.0.8" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz" integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/extensions/dart/cgmanifest.json b/extensions/dart/cgmanifest.json index 0086a5158e5..9c90588adf1 100644 --- a/extensions/dart/cgmanifest.json +++ b/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/extensions/dart/syntaxes/dart.tmLanguage.json b/extensions/dart/syntaxes/dart.tmLanguage.json index ae4db9698e9..cc9dee8d275 100644 --- a/extensions/dart/syntaxes/dart.tmLanguage.json +++ b/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": "(? 0) { + let rangeToReveal = previousVisibleRanges[0]; + if (previousSelection && previousVisibleRanges.length > 1) { + // In case of multiple visible ranges, find the one that intersects with the selection + rangeToReveal = previousVisibleRanges.find(r => r.intersection(previousSelection)) ?? rangeToReveal; + } + editor.revealRange(rangeToReveal); } } } @@ -1506,6 +1511,33 @@ export class CommandCenter { textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)]; } + @command('git.diff.stageHunk') + async diffStageHunk(changes: DiffEditorSelectionHunkToolbarContext): Promise { + this.diffStageHunkOrSelection(changes); + } + + @command('git.diff.stageSelection') + async diffStageSelection(changes: DiffEditorSelectionHunkToolbarContext): Promise { + this.diffStageHunkOrSelection(changes); + } + + async diffStageHunkOrSelection(changes: DiffEditorSelectionHunkToolbarContext): Promise { + let modifiedUri = changes.modifiedUri; + if (!modifiedUri) { + const textEditor = window.activeTextEditor; + if (!textEditor) { + return; + } + const modifiedDocument = textEditor.document; + 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/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 5167b1eb95e..3f8553260e9 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode'; +import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n } from 'vscode'; import * as path from 'path'; import { Repository, GitResourceGroup } from './repository'; import { Model } from './model'; import { debounce } from './decorators'; -import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable } from './util'; -import { GitErrorCodes, Status } from './api/git'; +import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable, runAndSubscribeEvent } from './util'; +import { Change, GitErrorCodes, Status } from './api/git'; class GitIgnoreDecorationProvider implements FileDecorationProvider { @@ -101,7 +101,7 @@ class GitDecorationProvider implements FileDecorationProvider { constructor(private repository: Repository) { this.disposables.push( window.registerFileDecorationProvider(this), - repository.onDidRunGitStatus(this.onDidRunGitStatus, this) + runAndSubscribeEvent(repository.onDidRunGitStatus, () => this.onDidRunGitStatus()) ); } @@ -153,100 +153,97 @@ 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), + runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemGroup, () => this.onDidChangeCurrentHistoryItemGroup()) + ); + } + + 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 +284,7 @@ export class GitDecorations { private onDidOpenRepository(repository: Repository): void { const providers = combinedDisposable([ new GitDecorationProvider(repository), - // new GitIncomingChangesFileDecorationProvider(repository) + new GitIncomingChangesFileDecorationProvider(repository) ]); this.providers.set(repository, providers); diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index 7829483dc98..af80924ae13 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -63,9 +63,14 @@ export class GitFileSystemProvider implements FileSystemProvider { return; } - const gitUri = toGitUri(uri, '', { replaceFileExtension: true }); + const diffOriginalResourceUri = toGitUri(uri, '~',); + const quickDiffOriginalResourceUri = toGitUri(uri, '', { replaceFileExtension: true }); + this.mtime = new Date().getTime(); - this._onDidChangeFile.fire([{ type: FileChangeType.Changed, uri: gitUri }]); + this._onDidChangeFile.fire([ + { type: FileChangeType.Changed, uri: diffOriginalResourceUri }, + { type: FileChangeType.Changed, uri: quickDiffOriginalResourceUri } + ]); } @debounce(1100) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 738295bdec1..710d7a4d110 100644 --- a/extensions/git/src/git.ts +++ b/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/extensions/git/src/protocolHandler.ts b/extensions/git/src/protocolHandler.ts index 00f58173014..dc73fe39965 100644 --- a/extensions/git/src/protocolHandler.ts +++ b/extensions/git/src/protocolHandler.ts @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { UriHandler, Uri, window, Disposable, commands, LogOutputChannel, l10n } from 'vscode'; -import { dispose } from './util'; +import { dispose, isWindows } from './util'; import * as querystring from 'querystring'; -const schemes = new Set(['file', 'git', 'http', 'https', 'ssh']); +const schemes = isWindows ? + new Set(['git', 'http', 'https', 'ssh']) : + new Set(['file', 'git', 'http', 'https', 'ssh']); + const refRegEx = /^$|[~\^:\\\*\s\[\]]|^-|^\.|\/\.|\.\.|\.lock\/|\.lock$|\/$|\.$/; export class GitProtocolHandler implements UriHandler { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index edd250797e0..7f29a213ed3 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1166,9 +1166,10 @@ export class Repository implements Disposable { const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); await this.run(Operation.Stage, async () => { await this.repository.stage(path, contents); + + this._onDidChangeOriginalResource.fire(resource); this.closeDiffEditors([], [...resource.fsPath]); }); - this._onDidChangeOriginalResource.fire(resource); } async revert(resources: Uri[]): Promise { diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index 2813bfb1ee9..2dcc6d54487 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Range, LineChange, Selection } from 'vscode'; +import { TextDocument, Range, LineChange, Selection, Uri } from 'vscode'; export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string { const result: string[] = []; @@ -142,3 +142,14 @@ 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; + + modifiedUri: Uri; + originalUri: Uri; +} diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 0d4b9241b55..219c87b148d 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -47,6 +47,13 @@ export function filterEvent(event: Event, filter: (e: T) => boolean): Even return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables); } +export function runAndSubscribeEvent(event: Event, handler: (e: T) => any, initial: T): IDisposable; +export function runAndSubscribeEvent(event: Event, handler: (e: T | undefined) => any): IDisposable; +export function runAndSubscribeEvent(event: Event, handler: (e: T | undefined) => any, initial?: T): IDisposable { + handler(initial); + return event(e => handler(e)); +} + export function anyEvent(...events: Event[]): Event { return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => { const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i)))); diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index dc26d5c07e8..d55e8dcfd03 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -61,7 +61,7 @@ "dependencies": { "node-fetch": "2.6.7", "@vscode/extension-telemetry": "^0.9.0", - "vscode-tas-client": "^0.1.47" + "vscode-tas-client": "^0.1.84" }, "devDependencies": { "@types/mocha": "^9.1.1", diff --git a/extensions/github-authentication/src/common/errors.ts b/extensions/github-authentication/src/common/errors.ts index 3ba3dfc006a..f60b7233499 100644 --- a/extensions/github-authentication/src/common/errors.ts +++ b/extensions/github-authentication/src/common/errors.ts @@ -8,3 +8,7 @@ export const TIMED_OUT_ERROR = 'Timed out'; // These error messages are internal and should not be shown to the user in any way. export const USER_CANCELLATION_ERROR = 'User Cancelled'; export const NETWORK_ERROR = 'network error'; + +// This is the error message that we throw if the login was cancelled for any reason. Extensions +// calling `getSession` can handle this error to know that the user cancelled the login. +export const CANCELLATION_ERROR = 'Cancelled'; diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 3641ffb3a36..7498a2b2202 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -68,6 +68,7 @@ interface IFlowTriggerOptions { callbackUri: Uri; uriHandler: UriEventHandler; enterpriseUri?: Uri; + existingLogin?: string; } interface IFlow { @@ -149,7 +150,8 @@ const allFlows: IFlow[] = [ nonce, callbackUri, uriHandler, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying without local server... (${scopes})`); return await window.withProgress({ @@ -169,6 +171,9 @@ const allFlows: IFlow[] = [ ['scope', scopes], ['state', encodeURIComponent(callbackUri.toString(true))] ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } // The extra toString, parse is apparently needed for env.openExternal // to open the correct URL. @@ -215,7 +220,8 @@ const allFlows: IFlow[] = [ baseUri, redirectUri, logger, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying with local server... (${scopes})`); return await window.withProgress({ @@ -232,6 +238,9 @@ const allFlows: IFlow[] = [ ['redirect_uri', redirectUri.toString(true)], ['scope', scopes], ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } const loginUrl = baseUri.with({ path: '/login/oauth/authorize', diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 71aa17bd5cc..3d73bfb7656 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -11,7 +11,7 @@ import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './common/experimentationService'; import { Log } from './common/logger'; import { crypto } from './node/crypto'; -import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -296,13 +296,44 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid scopes: JSON.stringify(scopes), }); + const sessions = await this._sessionsPromise; const scopeString = sortedScopes.join(' '); - const token = await this._githubServer.login(scopeString); + const existingLogin = sessions[0]?.account.label; + const token = await this._githubServer.login(scopeString, existingLogin); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - const sessions = await this._sessionsPromise; + if (sessions.some(s => s.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/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 0729c4c5077..af2cf22724f 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -11,19 +11,15 @@ import { isSupportedClient, isSupportedTarget } from './common/env'; import { crypto } from './node/crypto'; import { fetching } from './node/fetch'; import { ExtensionHost, GitHubTarget, getFlows } from './flows'; -import { NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; import { Config } from './config'; import { base64Encode } from './node/buffer'; -// This is the error message that we throw if the login was cancelled for any reason. Extensions -// calling `getSession` can handle this error to know that the user cancelled the login. -const CANCELLATION_ERROR = 'Cancelled'; - const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect'; const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; export interface IGitHubServer { - login(scopes: string): Promise; + login(scopes: string, existingLogin?: string): Promise; logout(session: vscode.AuthenticationSession): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise; @@ -91,7 +87,7 @@ export class GitHubServer implements IGitHubServer { return this._isNoCorsEnvironment; } - public async login(scopes: string): Promise { + public async login(scopes: string, existingLogin?: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); // Used for showing a friendlier message to the user when the explicitly cancel a flow. @@ -143,6 +139,7 @@ export class GitHubServer implements IGitHubServer { uriHandler: this._uriHandler, enterpriseUri: this._ghesUri, redirectUri: vscode.Uri.parse(await this.getRedirectEndpoint()), + existingLogin }); } catch (e) { userCancelled = this.processLoginError(e); diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index a1c68b8d5a6..724b304c53e 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -132,15 +132,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -axios@^1.6.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" - integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== - dependencies: - follow-redirects "^1.15.0" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -153,11 +144,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -follow-redirects@^1.15.0: - version "1.15.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" - integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== - form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -167,15 +153,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" @@ -195,29 +172,22 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -tas-client@0.1.73: - version "0.1.73" - resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.1.73.tgz#2dacf68547a37989ef1554c6510dc108a1ea7a71" - integrity sha512-UDdUF9kV2hYdlv+7AgqP2kXarVSUhjK7tg1BUflIRGEgND0/QoNpN64rcEuhEcM8AIbW65yrCopJWqRhLZ3m8w== - dependencies: - axios "^1.6.1" +tas-client@0.2.33: + version "0.2.33" + resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.2.33.tgz#451bf114a8a64748030ce4068ab7d079958402e6" + integrity sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg== tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -vscode-tas-client@^0.1.47: - version "0.1.75" - resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.75.tgz#771780a9a178163028299f52d41973300060dd38" - integrity sha512-/+ALFWPI4U3obeRvLFSt39guT7P9bZQrkmcLoiS+2HtzJ/7iPKNt5Sj+XTiitGlPYVFGFc0plxX8AAp6Uxs0xQ== +vscode-tas-client@^0.1.84: + version "0.1.84" + resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" + integrity sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w== dependencies: - tas-client "0.1.73" + tas-client "0.2.33" webidl-conversions@^3.0.0: version "3.0.1" diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index 2b837e80a2f..7b7bc3d51f9 100644 --- a/extensions/go/cgmanifest.json +++ b/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/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index 3641d3edf99..efd69afbcd2 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/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/extensions/ipynb/src/common.ts b/extensions/ipynb/src/common.ts index d5ff5f86069..a25973e95a6 100644 --- a/extensions/ipynb/src/common.ts +++ b/extensions/ipynb/src/common.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as nbformat from '@jupyterlab/nbformat'; +import { workspace } from 'vscode'; /** * Metadata we store in VS Code cell output items. @@ -58,5 +59,13 @@ export interface CellMetadata { /** * Stores cell metadata. */ - metadata?: Partial; + metadata?: Partial & { vscode?: { languageId?: string } }; + /** + * The code cell's prompt number. Will be null if the cell has not been run. + */ + execution_count?: number; +} + +export function useCustomPropertyInMetadata() { + return !workspace.getConfiguration('jupyter', undefined).get('experimental.dropCustomMetadata', false); } diff --git a/extensions/ipynb/src/deserializers.ts b/extensions/ipynb/src/deserializers.ts index 21dd078b89b..920689c748c 100644 --- a/extensions/ipynb/src/deserializers.ts +++ b/extensions/ipynb/src/deserializers.ts @@ -5,7 +5,7 @@ import type * as nbformat from '@jupyterlab/nbformat'; import { extensions, NotebookCellData, NotebookCellExecutionSummary, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData } from 'vscode'; -import { CellMetadata, CellOutputMetadata } from './common'; +import { CellMetadata, CellOutputMetadata, useCustomPropertyInMetadata } from './common'; const jupyterLanguageToMonacoLanguageMapping = new Map([ ['c#', 'csharp'], @@ -154,24 +154,51 @@ function convertJupyterOutputToBuffer(mime: string, value: unknown): NotebookCel function getNotebookCellMetadata(cell: nbformat.IBaseCell): { [key: string]: any; } { - const cellMetadata: { [key: string]: any } = {}; - // We put this only for VSC to display in diff view. - // Else we don't use this. - const custom: CellMetadata = {}; - if (cell['metadata']) { - custom['metadata'] = JSON.parse(JSON.stringify(cell['metadata'])); - } + if (useCustomPropertyInMetadata()) { + const cellMetadata: { [key: string]: any } = {}; + // We put this only for VSC to display in diff view. + // Else we don't use this. + const custom: CellMetadata = {}; + + if (cell.cell_type === 'code' && typeof cell['execution_count'] === 'number') { + custom.execution_count = cell['execution_count']; + } - if ('id' in cell && typeof cell.id === 'string') { - custom.id = cell.id; - } + if (cell['metadata']) { + custom['metadata'] = JSON.parse(JSON.stringify(cell['metadata'])); + } - cellMetadata.custom = custom; + if ('id' in cell && typeof cell.id === 'string') { + custom.id = cell.id; + } + + cellMetadata.custom = custom; - if (cell['attachments']) { - cellMetadata.attachments = JSON.parse(JSON.stringify(cell['attachments'])); + if (cell['attachments']) { + cellMetadata.attachments = JSON.parse(JSON.stringify(cell['attachments'])); + } + return cellMetadata; + } else { + // We put this only for VSC to display in diff view. + // Else we don't use this. + const cellMetadata: CellMetadata = {}; + if (cell.cell_type === 'code' && typeof cell['execution_count'] === 'number') { + cellMetadata.execution_count = cell['execution_count']; + } + + if (cell['metadata']) { + cellMetadata['metadata'] = JSON.parse(JSON.stringify(cell['metadata'])); + } + + if ('id' in cell && typeof cell.id === 'string') { + cellMetadata.id = cell.id; + } + + if (cell['attachments']) { + cellMetadata.attachments = JSON.parse(JSON.stringify(cell['attachments'])); + } + return cellMetadata; } - return cellMetadata; } function getOutputMetadata(output: nbformat.IOutput): CellOutputMetadata { @@ -364,6 +391,6 @@ export function jupyterNotebookModelToNotebookData( .filter((item): item is NotebookCellData => !!item); const notebookData = new NotebookData(cells); - notebookData.metadata = { custom: notebookContentWithoutCells }; + notebookData.metadata = useCustomPropertyInMetadata() ? { custom: notebookContentWithoutCells } : notebookContentWithoutCells; return notebookData; } diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 01efb4826d4..114125b289b 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -5,9 +5,10 @@ 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'; +import { useCustomPropertyInMetadata } from './common'; // --- Start Positron --- import * as positron from 'positron'; @@ -34,13 +35,18 @@ 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: { + transientCellMetadata: useCustomPropertyInMetadata() ? { breakpointMargin: true, custom: false, attachments: false + } : { + breakpointMargin: true, + id: false, + metadata: false, + attachments: false }, cellContentMetadata: { attachments: true @@ -49,10 +55,15 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.workspace.registerNotebookSerializer('interactive', serializer, { transientOutputs: false, - transientCellMetadata: { + transientCellMetadata: useCustomPropertyInMetadata() ? { breakpointMargin: true, custom: false, attachments: false + } : { + breakpointMargin: true, + id: false, + metadata: false, + attachments: false }, cellContentMetadata: { attachments: true @@ -80,13 +91,18 @@ export function activate(context: vscode.ExtensionContext) { // --- End Positron --- const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '', language); const data = new vscode.NotebookData([cell]); - data.metadata = { + data.metadata = useCustomPropertyInMetadata() ? { custom: { cells: [], metadata: {}, nbformat: 4, nbformat_minor: 2 } + } : { + cells: [], + metadata: {}, + nbformat: 4, + nbformat_minor: 2 }; const doc = await vscode.workspace.openNotebookDocument('jupyter-notebook', data); await vscode.window.showNotebookDocument(doc); @@ -116,6 +132,9 @@ export function activate(context: vscode.ExtensionContext) { return { + get dropCustomMetadata() { + return !useCustomPropertyInMetadata(); + }, exportNotebook: (notebook: vscode.NotebookData): string => { return exportNotebook(notebook, serializer); }, @@ -126,16 +145,26 @@ export function activate(context: vscode.ExtensionContext) { } const edit = new vscode.WorkspaceEdit(); - edit.set(resource, [vscode.NotebookEdit.updateNotebookMetadata({ - ...document.metadata, - custom: { - ...(document.metadata.custom ?? {}), + if (useCustomPropertyInMetadata()) { + edit.set(resource, [vscode.NotebookEdit.updateNotebookMetadata({ + ...document.metadata, + custom: { + ...(document.metadata.custom ?? {}), + metadata: { + ...(document.metadata.custom?.metadata ?? {}), + ...metadata + }, + } + })]); + } else { + edit.set(resource, [vscode.NotebookEdit.updateNotebookMetadata({ + ...document.metadata, metadata: { - ...(document.metadata.custom?.metadata ?? {}), + ...(document.metadata.metadata ?? {}), ...metadata }, - } - })]); + })]); + } return vscode.workspace.applyEdit(edit); }, }; diff --git a/extensions/ipynb/src/notebookAttachmentCleaner.ts b/extensions/ipynb/src/notebookAttachmentCleaner.ts index cad19f07b29..32aae0c5d1e 100644 --- a/extensions/ipynb/src/notebookAttachmentCleaner.ts +++ b/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/extensions/ipynb/src/notebookImagePaste.ts b/extensions/ipynb/src/notebookImagePaste.ts index 94292c26a74..7ea63e7026a 100644 --- a/extensions/ipynb/src/notebookImagePaste.ts +++ b/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/extensions/ipynb/src/notebookModelStoreSync.ts b/extensions/ipynb/src/notebookModelStoreSync.ts new file mode 100644 index 00000000000..737034266f0 --- /dev/null +++ b/extensions/ipynb/src/notebookModelStoreSync.ts @@ -0,0 +1,251 @@ +/*--------------------------------------------------------------------------------------------- + * 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, sortObjectPropertiesRecursively } from './serializers'; +import { CellMetadata, useCustomPropertyInMetadata } 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, updates: { cell: NotebookCell; metadata: CellMetadata & { vscode?: { languageId: string } } }[]) { + const pendingUpdates = pendingNotebookCellModelUpdates.get(notebook) ?? new Set>(); + pendingNotebookCellModelUpdates.set(notebook, pendingUpdates); + const edit = new WorkspaceEdit(); + updates.forEach(({ cell, metadata }) => { + let newMetadata: any = {}; + if (useCustomPropertyInMetadata()) { + newMetadata = { ...(cell.metadata), custom: metadata }; + } else { + newMetadata = { ...cell.metadata, ...metadata }; + if (!metadata.execution_count && newMetadata.execution_count) { + delete newMetadata.execution_count; + } + if (!metadata.attachments && newMetadata.attachments) { + delete newMetadata.attachments; + } + } + edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, sortObjectPropertiesRecursively(newMetadata))]); + }); + 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; + const updates: { cell: NotebookCell; metadata: CellMetadata & { vscode?: { languageId: string } } }[] = []; + // 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 currentMetadata = e.metadata ? getCellMetadata({ metadata: e.metadata }) : getCellMetadata({ cell: e.cell }); + const languageIdInMetadata = getVSCodeCellLanguageId(currentMetadata); + const metadata: CellMetadata = JSON.parse(JSON.stringify(currentMetadata)); + metadata.metadata = metadata.metadata || {}; + let metadataUpdated = false; + if (e.executionSummary?.executionOrder && typeof e.executionSummary.success === 'boolean' && currentMetadata.execution_count !== e.executionSummary?.executionOrder) { + metadata.execution_count = e.executionSummary.executionOrder; + metadataUpdated = true; + } else if (!e.executionSummary && !e.metadata && e.outputs?.length === 0 && currentMetadata.execution_count) { + // Clear all. + delete metadata.execution_count; + metadataUpdated = true; + } + + if (e.document?.languageId && e.document?.languageId !== preferredCellLanguage && e.document?.languageId !== languageIdInMetadata) { + setVSCodeCellLanguageId(metadata, e.document.languageId); + metadataUpdated = true; + } else if (e.document?.languageId && e.document.languageId === preferredCellLanguage && languageIdInMetadata) { + removeVSCodeCellLanguageId(metadata); + metadataUpdated = true; + } else if (e.document?.languageId && e.document.languageId === preferredCellLanguage && e.document.languageId === languageIdInMetadata) { + removeVSCodeCellLanguageId(metadata); + metadataUpdated = true; + } + + if (metadataUpdated) { + updates.push({ cell: 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); + } + updates.push({ cell, metadata }); + }); + }); + + if (updates.length) { + trackAndUpdateCellMetadata(notebook, updates); + } +} + + +/** + * 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/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index 1677c4d7c70..c6cbf722387 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -10,6 +10,7 @@ import { defaultNotebookFormat } from './constants'; import { getPreferredLanguage, jupyterNotebookModelToNotebookData } from './deserializers'; import { createJupyterCellFromNotebookCell, pruneCell, sortObjectPropertiesRecursively } from './serializers'; import * as fnv from '@enonic/fnv-plus'; +import { useCustomPropertyInMetadata } from './common'; export class NotebookSerializer implements vscode.NotebookSerializer { constructor(readonly context: vscode.ExtensionContext) { @@ -107,10 +108,11 @@ export class NotebookSerializer implements vscode.NotebookSerializer { } export function getNotebookMetadata(document: vscode.NotebookDocument | vscode.NotebookData) { - const notebookContent: Partial = document.metadata?.custom || {}; - notebookContent.cells = notebookContent.cells || []; - notebookContent.nbformat = notebookContent.nbformat || defaultNotebookFormat.major; - notebookContent.nbformat_minor = notebookContent.nbformat_minor ?? defaultNotebookFormat.minor; - notebookContent.metadata = notebookContent.metadata || {}; + const existingContent: Partial = (useCustomPropertyInMetadata() ? document.metadata?.custom : document.metadata) || {}; + const notebookContent: Partial = {}; + notebookContent.cells = existingContent.cells || []; + notebookContent.nbformat = existingContent.nbformat || defaultNotebookFormat.major; + notebookContent.nbformat_minor = existingContent.nbformat_minor ?? defaultNotebookFormat.minor; + notebookContent.metadata = existingContent.metadata || {}; return notebookContent; } diff --git a/extensions/ipynb/src/serializers.ts b/extensions/ipynb/src/serializers.ts index 27c45bce918..3eb6e90eabd 100644 --- a/extensions/ipynb/src/serializers.ts +++ b/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, useCustomPropertyInMetadata, type CellMetadata } from './common'; import { textMimeTypes } from './deserializers'; const textDecoder = new TextDecoder(); @@ -54,28 +54,71 @@ export function sortObjectPropertiesRecursively(obj: any): any { return obj; } -export function getCellMetadata(cell: NotebookCell | NotebookCellData) { - return { - // 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 - }; +export function getCellMetadata(options: { cell: NotebookCell | NotebookCellData } | { metadata?: { [key: string]: any } }): CellMetadata { + if ('cell' in options) { + const cell = options.cell; + if (useCustomPropertyInMetadata()) { + const metadata: CellMetadata = { + // 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 + const attachments = cell.metadata?.custom?.attachments ?? cell.metadata?.attachments; + if (attachments) { + metadata.attachments = attachments; + } + return metadata; + } + const metadata = { + // it contains the cell id, and the cell metadata, along with other nb cell metadata + ...(cell.metadata ?? {}) + }; + + return metadata; + } else { + const cell = options; + if (useCustomPropertyInMetadata()) { + const metadata: CellMetadata = { + // 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 + const attachments = cell.metadata?.custom?.attachments ?? cell.metadata?.attachments; + if (attachments) { + metadata.attachments = attachments; + } + return metadata; + } + const metadata = { + // it contains the cell id, and the cell metadata, along with other nb cell metadata + ...(cell.metadata ?? {}) + }; + + 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 +126,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; @@ -92,7 +135,7 @@ function createCodeCellFromNotebookCell(cell: NotebookCellData, preferredLanguag } function createRawCellFromNotebookCell(cell: NotebookCellData): nbformat.IRawCell { - const cellMetadata = getCellMetadata(cell); + const cellMetadata = getCellMetadata({ cell }); const rawCell: any = { cell_type: 'raw', source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')), @@ -343,7 +386,7 @@ function convertOutputMimeToJupyterOutput(mime: string, value: Uint8Array) { } export function createMarkdownCellFromNotebookCell(cell: NotebookCellData): nbformat.IMarkdownCell { - const cellMetadata = getCellMetadata(cell); + const cellMetadata = getCellMetadata({ cell }); const markdownCell: any = { cell_type: 'markdown', source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')), diff --git a/extensions/ipynb/src/test/notebookModelStoreSync.test.ts b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts new file mode 100644 index 00000000000..1afca8c1239 --- /dev/null +++ b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts @@ -0,0 +1,680 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +[true, false].forEach(useCustomPropertyInMetadata => { + suite(`Notebook Model Store Sync (${useCustomPropertyInMetadata ? 'with custom metadata (standard behaviour)' : 'without custom metadata'})`, () => { + 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; + sinon.stub(workspace, 'getConfiguration').callsFake((section, scope) => { + if (section === 'jupyter') { + return { + get: () => { + return !useCustomPropertyInMetadata; + } + }; + } else { + return (workspace.getConfiguration as any).wrappedMethod.call(workspace, section, scope); + } + }); + 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; + if (useCustomPropertyInMetadata) { + assert.deepStrictEqual(newMetadata, { custom: { metadata: {} } }); + } else { + assert.deepStrictEqual(newMetadata, { metadata: {} }); + } + }); + test('Add cell id if nbformat is 4.5', async () => { + if (useCustomPropertyInMetadata) { + sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); + } else { + sinon.stub(notebook, 'metadata').get(() => ({ 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 || {}; + if (useCustomPropertyInMetadata) { + 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); + } else { + assert.strictEqual(Object.keys(newMetadata).length, 2); + assert.deepStrictEqual(newMetadata.metadata, {}); + assert.ok(newMetadata.id); + } + }); + test('Do not add cell id if one already exists', async () => { + if (useCustomPropertyInMetadata) { + sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); + } else { + sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 })); + } + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: useCustomPropertyInMetadata ? { + custom: { + id: '1234' + } + } : { + 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 || {}; + if (useCustomPropertyInMetadata) { + 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'); + } else { + assert.strictEqual(Object.keys(newMetadata).length, 2); + assert.deepStrictEqual(newMetadata.metadata, {}); + assert.strictEqual(newMetadata.id, '1234'); + } + }); + test('Do not perform any updates if cell id and metadata exists', async () => { + if (useCustomPropertyInMetadata) { + sinon.stub(notebook, 'metadata').get(() => ({ custom: { nbformat: 4, nbformat_minor: 5 } })); + } else { + sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 })); + } + const cell: NotebookCell = { + document: {} as any, + executionSummary: {}, + index: 0, + kind: NotebookCellKind.Code, + metadata: useCustomPropertyInMetadata ? { + custom: { + id: '1234', + metadata: {} + } + } : { + 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 () => { + if (useCustomPropertyInMetadata) { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'python' } + } + } + })); + } else { + sinon.stub(notebook, 'metadata').get(() => ({ + 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: useCustomPropertyInMetadata ? { + custom: { + id: '1234', + metadata: { + collapsed: true, scrolled: true + } + } + } : { + id: '1234', + metadata: { + collapsed: true, scrolled: true + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [ + { + cell, + document: { + languageId: 'javascript' + } as any, + metadata: undefined, + outputs: undefined, + executionSummary: undefined + } + ] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + if (useCustomPropertyInMetadata) { + 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'); + } else { + assert.strictEqual(Object.keys(newMetadata).length, 2); + assert.deepStrictEqual(newMetadata.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'javascript' } }); + assert.strictEqual(newMetadata.id, '1234'); + } + }); + test('No changes when language is javascript', async () => { + if (useCustomPropertyInMetadata) { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'javascript' } + } + } + })); + } else { + sinon.stub(notebook, 'metadata').get(() => ({ + 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: useCustomPropertyInMetadata ? { + custom: { + id: '1234', + metadata: { + collapsed: true, scrolled: true + } + } + } : { + 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 () => { + if (useCustomPropertyInMetadata) { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'javascript' } + } + } + })); + } else { + sinon.stub(notebook, 'metadata').get(() => ({ + 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: useCustomPropertyInMetadata ? { + custom: { + id: '1234', + metadata: { + vscode: { languageId: 'python' }, + collapsed: true, scrolled: true + } + } + } : { + id: '1234', + metadata: { + vscode: { languageId: 'python' }, + collapsed: true, scrolled: true + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [ + { + cell, + document: { + languageId: 'javascript' + } as any, + metadata: undefined, + outputs: undefined, + executionSummary: undefined + } + ] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + if (useCustomPropertyInMetadata) { + 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'); + } else { + assert.strictEqual(Object.keys(newMetadata).length, 2); + assert.deepStrictEqual(newMetadata.metadata, { collapsed: true, scrolled: true }); + assert.strictEqual(newMetadata.id, '1234'); + } + }); + test('Update language in metadata', async () => { + if (useCustomPropertyInMetadata) { + sinon.stub(notebook, 'metadata').get(() => ({ + custom: { + nbformat: 4, nbformat_minor: 5, + metadata: { + language_info: { name: 'javascript' } + } + } + })); + } else { + + sinon.stub(notebook, 'metadata').get(() => ({ + 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: useCustomPropertyInMetadata ? { + custom: { + id: '1234', + metadata: { + vscode: { languageId: 'python' }, + collapsed: true, scrolled: true + } + } + } : { + id: '1234', + metadata: { + vscode: { languageId: 'python' }, + collapsed: true, scrolled: true + } + }, + notebook, + outputs: [] + }; + const e: NotebookDocumentChangeEvent = { + notebook, + metadata: undefined, + contentChanges: [], + cellChanges: [ + { + cell, + document: { + languageId: 'powershell' + } as any, + metadata: undefined, + outputs: undefined, + executionSummary: undefined + } + ] + }; + + onDidChangeNotebookDocument.fire(e); + + assert.strictEqual(editsApplied.length, 1); + assert.strictEqual(cellMetadataUpdates.length, 1); + const newMetadata = cellMetadataUpdates[0].newCellMetadata || {}; + if (useCustomPropertyInMetadata) { + 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'); + } else { + assert.strictEqual(Object.keys(newMetadata).length, 2); + assert.deepStrictEqual(newMetadata.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'powershell' } }); + assert.strictEqual(newMetadata.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/extensions/ipynb/src/test/serializers.test.ts b/extensions/ipynb/src/test/serializers.test.ts index b8f56fc5dcb..cc7f53fe442 100644 --- a/extensions/ipynb/src/test/serializers.test.ts +++ b/extensions/ipynb/src/test/serializers.test.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as sinon from 'sinon'; import type * as nbformat from '@jupyterlab/nbformat'; import * as assert from 'assert'; import * as vscode from 'vscode'; @@ -18,75 +19,119 @@ function deepStripProperties(obj: any, props: string[]) { } } } +[true, false].forEach(useCustomPropertyInMetadata => { + suite(`ipynb serializer (${useCustomPropertyInMetadata ? 'with custom metadata (standard behaviour)' : 'without custom metadata'})`, () => { + let disposables: vscode.Disposable[] = []; + setup(() => { + disposables = []; + sinon.stub(vscode.workspace, 'getConfiguration').callsFake((section, scope) => { + if (section === 'jupyter') { + return { + get: () => { + return !useCustomPropertyInMetadata; + } + }; + } else { + return (vscode.workspace.getConfiguration as any).wrappedMethod.call(vscode.workspace, section, scope); + } + }); + }); + teardown(async () => { + disposables.forEach(d => d.dispose()); + disposables = []; + sinon.restore(); + }); -suite('ipynb serializer', () => { - const base64EncodedImage = - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUlZL6DwAB/wFSU1jVmgAAAABJRU5ErkJggg=='; - test('Deserialize', async () => { - const cells: nbformat.ICell[] = [ - { - cell_type: 'code', - execution_count: 10, - outputs: [], - source: 'print(1)', - metadata: {} - }, - { - cell_type: 'markdown', - source: '# HEAD', - metadata: {} - } - ]; - const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); - assert.ok(notebook); + const base64EncodedImage = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUlZL6DwAB/wFSU1jVmgAAAABJRU5ErkJggg=='; + test('Deserialize', async () => { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs: [], + source: 'print(1)', + metadata: {} + }, + { + cell_type: 'markdown', + source: '# HEAD', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + assert.ok(notebook); - const expectedCodeCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(1)', 'python'); - expectedCodeCell.outputs = []; - expectedCodeCell.metadata = { custom: { metadata: {} } }; - expectedCodeCell.executionSummary = { executionOrder: 10 }; + const expectedCodeCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(1)', 'python'); + expectedCodeCell.outputs = []; + expectedCodeCell.metadata = useCustomPropertyInMetadata ? { custom: { execution_count: 10, metadata: {} } } : { execution_count: 10, metadata: {} }; + expectedCodeCell.executionSummary = { executionOrder: 10 }; - const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown'); - expectedMarkdownCell.outputs = []; - expectedMarkdownCell.metadata = { - custom: { metadata: {} } - }; + const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown'); + expectedMarkdownCell.outputs = []; + expectedMarkdownCell.metadata = useCustomPropertyInMetadata ? { + custom: { metadata: {} } + } : { + metadata: {} + }; - assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedMarkdownCell]); - }); + assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedMarkdownCell]); + }); - test('Serialize', async () => { - const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); - markdownCell.metadata = { - attachments: { - 'image.png': { - 'image/png': 'abc' + test('Serialize', async () => { + const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); + markdownCell.metadata = useCustomPropertyInMetadata ? { + attachments: { + 'image.png': { + 'image/png': 'abc' + } + }, + custom: { + id: '123', + metadata: { + foo: 'bar' + } } - }, - custom: { + } : { + attachments: { + 'image.png': { + 'image/png': 'abc' + } + }, id: '123', metadata: { foo: 'bar' } - } - }; + }; - const cellMetadata = getCellMetadata(markdownCell); - assert.deepStrictEqual(cellMetadata, { - id: '123', - metadata: { - foo: 'bar', - }, - attachments: { - 'image.png': { - 'image/png': 'abc' + const cellMetadata = getCellMetadata({ cell: markdownCell }); + assert.deepStrictEqual(cellMetadata, { + id: '123', + metadata: { + foo: 'bar', + }, + attachments: { + 'image.png': { + 'image/png': 'abc' + } } - } - }); + }); - const markdownCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); - markdownCell2.metadata = { - custom: { + const markdownCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); + markdownCell2.metadata = useCustomPropertyInMetadata ? { + custom: { + id: '123', + metadata: { + foo: 'bar' + }, + attachments: { + 'image.png': { + 'image/png': 'abc' + } + } + } + } : { id: '123', metadata: { foo: 'bar' @@ -96,132 +141,145 @@ suite('ipynb serializer', () => { 'image/png': 'abc' } } - } - }; + }; - const nbMarkdownCell = createMarkdownCellFromNotebookCell(markdownCell); - const nbMarkdownCell2 = createMarkdownCellFromNotebookCell(markdownCell2); - assert.deepStrictEqual(nbMarkdownCell, nbMarkdownCell2); + const nbMarkdownCell = createMarkdownCellFromNotebookCell(markdownCell); + const nbMarkdownCell2 = createMarkdownCellFromNotebookCell(markdownCell2); + assert.deepStrictEqual(nbMarkdownCell, nbMarkdownCell2); - assert.deepStrictEqual(nbMarkdownCell, { - cell_type: 'markdown', - source: ['# header1'], - metadata: { - foo: 'bar', - }, - attachments: { - 'image.png': { - 'image/png': 'abc' - } - }, - id: '123' + assert.deepStrictEqual(nbMarkdownCell, { + cell_type: 'markdown', + source: ['# header1'], + metadata: { + foo: 'bar', + }, + attachments: { + 'image.png': { + 'image/png': 'abc' + } + }, + id: '123' + }); }); - }); - suite('Outputs', () => { - function validateCellOutputTranslation( - outputs: nbformat.IOutput[], - expectedOutputs: vscode.NotebookCellOutput[], - propertiesToExcludeFromComparison: string[] = [] - ) { - const cells: nbformat.ICell[] = [ - { - cell_type: 'code', - execution_count: 10, - outputs, - source: 'print(1)', - metadata: {} - } - ]; - const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + suite('Outputs', () => { + function validateCellOutputTranslation( + outputs: nbformat.IOutput[], + expectedOutputs: vscode.NotebookCellOutput[], + propertiesToExcludeFromComparison: string[] = [] + ) { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs, + source: 'print(1)', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); - // OutputItems contain an `id` property generated by VSC. - // Exclude that property when comparing. - const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']); - const actualOuts = notebook.cells[0].outputs; - deepStripProperties(actualOuts, propertiesToExclude); - deepStripProperties(expectedOutputs, propertiesToExclude); - assert.deepStrictEqual(actualOuts, expectedOutputs); - } + // OutputItems contain an `id` property generated by VSC. + // Exclude that property when comparing. + const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']); + const actualOuts = notebook.cells[0].outputs; + deepStripProperties(actualOuts, propertiesToExclude); + deepStripProperties(expectedOutputs, propertiesToExclude); + assert.deepStrictEqual(actualOuts, expectedOutputs); + } - test('Empty output', () => { - validateCellOutputTranslation([], []); - }); + test('Empty output', () => { + validateCellOutputTranslation([], []); + }); - test('Stream output', () => { - validateCellOutputTranslation( - [ - { - output_type: 'stream', - name: 'stderr', - text: 'Error' - }, - { - output_type: 'stream', - name: 'stdout', - text: 'NoError' - } - ], - [ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], { - outputType: 'stream' - }), - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], { - outputType: 'stream' - }) - ] - ); - }); - test('Stream output and line endings', () => { - validateCellOutputTranslation( - [ - { - output_type: 'stream', - name: 'stdout', - text: [ - 'Line1\n', - '\n', - 'Line3\n', - 'Line4' - ] - } - ], - [ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], { - outputType: 'stream' - }) - ] - ); - validateCellOutputTranslation( - [ - { - output_type: 'stream', - name: 'stdout', - text: [ - 'Hello\n', - 'Hello\n', - 'Hello\n', - 'Hello\n', - 'Hello\n', - 'Hello\n' - ] - } - ], - [ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], { - outputType: 'stream' - }) - ] - ); - }); - test('Multi-line Stream output', () => { - validateCellOutputTranslation( - [ - { - name: 'stdout', - output_type: 'stream', - text: [ - 'Epoch 1/5\n', + test('Stream output', () => { + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stderr', + text: 'Error' + }, + { + output_type: 'stream', + name: 'stdout', + text: 'NoError' + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], { + outputType: 'stream' + }), + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], { + outputType: 'stream' + }) + ] + ); + }); + test('Stream output and line endings', () => { + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stdout', + text: [ + 'Line1\n', + '\n', + 'Line3\n', + 'Line4' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], { + outputType: 'stream' + }) + ] + ); + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stdout', + text: [ + 'Hello\n', + 'Hello\n', + 'Hello\n', + 'Hello\n', + 'Hello\n', + 'Hello\n' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], { + outputType: 'stream' + }) + ] + ); + }); + test('Multi-line Stream output', () => { + validateCellOutputTranslation( + [ + { + name: 'stdout', + output_type: 'stream', + text: [ + 'Epoch 1/5\n', + '...\n', + 'Epoch 2/5\n', + '...\n', + 'Epoch 3/5\n', + '...\n', + 'Epoch 4/5\n', + '...\n', + 'Epoch 5/5\n', + '...\n' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n', '...\n', 'Epoch 2/5\n', '...\n', @@ -230,35 +288,35 @@ suite('ipynb serializer', () => { 'Epoch 4/5\n', '...\n', 'Epoch 5/5\n', - '...\n' - ] - } - ], - [ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n', - '...\n', - 'Epoch 2/5\n', - '...\n', - 'Epoch 3/5\n', - '...\n', - 'Epoch 4/5\n', - '...\n', - 'Epoch 5/5\n', - '...\n'].join(''))], { - outputType: 'stream' - }) - ] - ); - }); + '...\n'].join(''))], { + outputType: 'stream' + }) + ] + ); + }); - test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => { - validateCellOutputTranslation( - [ - { - name: 'stderr', - output_type: 'stream', - text: [ - 'Epoch 1/5\n', + test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + output_type: 'stream', + text: [ + 'Epoch 1/5\n', + '...\n', + 'Epoch 2/5\n', + '...\n', + 'Epoch 3/5\n', + '...\n', + 'Epoch 4/5\n', + '...\n', + 'Epoch 5/5\n', + '...\n' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n', '...\n', 'Epoch 2/5\n', '...\n', @@ -267,436 +325,423 @@ suite('ipynb serializer', () => { 'Epoch 4/5\n', '...\n', 'Epoch 5/5\n', - '...\n' - ] - } - ], - [ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n', - '...\n', - 'Epoch 2/5\n', - '...\n', - 'Epoch 3/5\n', - '...\n', - 'Epoch 4/5\n', - '...\n', - 'Epoch 5/5\n', - '...\n', - // This last empty line should not be saved in ipynb. - '\n'].join(''))], { - outputType: 'stream' - }) - ] - ); - }); + '...\n', + // This last empty line should not be saved in ipynb. + '\n'].join(''))], { + outputType: 'stream' + }) + ] + ); + }); - test('Streamed text with Ansi characters', async () => { - validateCellOutputTranslation( - [ - { - name: 'stderr', - text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n', - output_type: 'stream' - } - ], - [ - new vscode.NotebookCellOutput( - [vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + test('Streamed text with Ansi characters', async () => { + validateCellOutputTranslation( + [ { - outputType: 'stream' + name: 'stderr', + text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' } - ) - ] - ); - }); - - test('Streamed text with angle bracket characters', async () => { - validateCellOutputTranslation( - [ - { - name: 'stderr', - text: '1 is < 2', - output_type: 'stream' - } - ], - [ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], { - outputType: 'stream' - }) - ] - ); - }); + ], + [ + new vscode.NotebookCellOutput( + [vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + { + outputType: 'stream' + } + ) + ] + ); + }); - test('Streamed text with angle bracket characters and ansi chars', async () => { - validateCellOutputTranslation( - [ - { - name: 'stderr', - text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n', - output_type: 'stream' - } - ], - [ - new vscode.NotebookCellOutput( - [vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + test('Streamed text with angle bracket characters', async () => { + validateCellOutputTranslation( + [ { - outputType: 'stream' + name: 'stderr', + text: '1 is < 2', + output_type: 'stream' } - ) - ] - ); - }); + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], { + outputType: 'stream' + }) + ] + ); + }); - test('Error', async () => { - validateCellOutputTranslation( - [ - { - ename: 'Error Name', - evalue: 'Error Value', - traceback: ['stack1', 'stack2', 'stack3'], - output_type: 'error' - } - ], - [ - new vscode.NotebookCellOutput( - [ - vscode.NotebookCellOutputItem.error({ - name: 'Error Name', - message: 'Error Value', - stack: ['stack1', 'stack2', 'stack3'].join('\n') - }) - ], + test('Streamed text with angle bracket characters and ansi chars', async () => { + validateCellOutputTranslation( + [ { - outputType: 'error', - originalError: { - ename: 'Error Name', - evalue: 'Error Value', - traceback: ['stack1', 'stack2', 'stack3'], - output_type: 'error' - } + name: 'stderr', + text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' } - ) - ] - ); - }); - - ['display_data', 'execute_result'].forEach(output_type => { - suite(`Rich output for output_type = ${output_type}`, () => { - // Properties to exclude when comparing. - let propertiesToExcludeFromComparison: string[] = []; - setup(() => { - if (output_type === 'display_data') { - // With display_data the execution_count property will never exist in the output. - // We can ignore that (as it will never exist). - // But we leave it in the case of `output_type === 'execute_result'` - propertiesToExcludeFromComparison = ['execution_count', 'executionCount']; - } - }); + ], + [ + new vscode.NotebookCellOutput( + [vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + { + outputType: 'stream' + } + ) + ] + ); + }); - test('Text mimeType output', async () => { - validateCellOutputTranslation( - [ + test('Error', async () => { + validateCellOutputTranslation( + [ + { + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'], + output_type: 'error' + } + ], + [ + new vscode.NotebookCellOutput( + [ + vscode.NotebookCellOutputItem.error({ + name: 'Error Name', + message: 'Error Value', + stack: ['stack1', 'stack2', 'stack3'].join('\n') + }) + ], { - data: { - 'text/plain': 'Hello World!' - }, - output_type, - metadata: {}, - execution_count: 1 + outputType: 'error', + originalError: { + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'], + output_type: 'error' + } } - ], - [ - new vscode.NotebookCellOutput( - [new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')], + ) + ] + ); + }); + + ['display_data', 'execute_result'].forEach(output_type => { + suite(`Rich output for output_type = ${output_type}`, () => { + // Properties to exclude when comparing. + let propertiesToExcludeFromComparison: string[] = []; + setup(() => { + if (output_type === 'display_data') { + // With display_data the execution_count property will never exist in the output. + // We can ignore that (as it will never exist). + // But we leave it in the case of `output_type === 'execute_result'` + propertiesToExcludeFromComparison = ['execution_count', 'executionCount']; + } + }); + + test('Text mimeType output', async () => { + validateCellOutputTranslation( + [ { - outputType: output_type, - metadata: {}, // display_data & execute_result always have metadata. - executionCount: 1 + data: { + 'text/plain': 'Hello World!' + }, + output_type, + metadata: {}, + execution_count: 1 } - ) - ], - propertiesToExcludeFromComparison - ); - }); + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')], + { + outputType: output_type, + metadata: {}, // display_data & execute_result always have metadata. + executionCount: 1 + } + ) + ], + propertiesToExcludeFromComparison + ); + }); - test('png,jpeg images', async () => { - validateCellOutputTranslation( - [ - { - execution_count: 1, - data: { - 'image/png': base64EncodedImage, - 'image/jpeg': base64EncodedImage - }, - metadata: {}, - output_type - } - ], - [ - new vscode.NotebookCellOutput( - [ - new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'), - new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg') - ], + test('png,jpeg images', async () => { + validateCellOutputTranslation( + [ { - executionCount: 1, - outputType: output_type, - metadata: {} // display_data & execute_result always have metadata. + execution_count: 1, + data: { + 'image/png': base64EncodedImage, + 'image/jpeg': base64EncodedImage + }, + metadata: {}, + output_type } - ) - ], - propertiesToExcludeFromComparison - ); - }); + ], + [ + new vscode.NotebookCellOutput( + [ + new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'), + new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg') + ], + { + executionCount: 1, + outputType: output_type, + metadata: {} // display_data & execute_result always have metadata. + } + ) + ], + propertiesToExcludeFromComparison + ); + }); - test('png image with a light background', async () => { - validateCellOutputTranslation( - [ - { - execution_count: 1, - data: { - 'image/png': base64EncodedImage - }, - metadata: { - needs_background: 'light' - }, - output_type - } - ], - [ - new vscode.NotebookCellOutput( - [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + test('png image with a light background', async () => { + validateCellOutputTranslation( + [ { - executionCount: 1, + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, metadata: { needs_background: 'light' }, - outputType: output_type + output_type } - ) - ], - propertiesToExcludeFromComparison - ); - }); + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + needs_background: 'light' + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); - test('png image with a dark background', async () => { - validateCellOutputTranslation( - [ - { - execution_count: 1, - data: { - 'image/png': base64EncodedImage - }, - metadata: { - needs_background: 'dark' - }, - output_type - } - ], - [ - new vscode.NotebookCellOutput( - [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + test('png image with a dark background', async () => { + validateCellOutputTranslation( + [ { - executionCount: 1, + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, metadata: { needs_background: 'dark' }, - outputType: output_type + output_type } - ) - ], - propertiesToExcludeFromComparison - ); - }); + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + needs_background: 'dark' + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); - test('png image with custom dimensions', async () => { - validateCellOutputTranslation( - [ - { - execution_count: 1, - data: { - 'image/png': base64EncodedImage - }, - metadata: { - 'image/png': { height: '111px', width: '999px' } - }, - output_type - } - ], - [ - new vscode.NotebookCellOutput( - [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + test('png image with custom dimensions', async () => { + validateCellOutputTranslation( + [ { - executionCount: 1, + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, metadata: { 'image/png': { height: '111px', width: '999px' } }, - outputType: output_type + output_type } - ) - ], - propertiesToExcludeFromComparison - ); - }); + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + 'image/png': { height: '111px', width: '999px' } + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); - test('png allowed to scroll', async () => { - validateCellOutputTranslation( - [ - { - execution_count: 1, - data: { - 'image/png': base64EncodedImage - }, - metadata: { - unconfined: true, - 'image/png': { width: '999px' } - }, - output_type - } - ], - [ - new vscode.NotebookCellOutput( - [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + test('png allowed to scroll', async () => { + validateCellOutputTranslation( + [ { - executionCount: 1, + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, metadata: { unconfined: true, 'image/png': { width: '999px' } }, - outputType: output_type + output_type } - ) - ], - propertiesToExcludeFromComparison - ); + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + unconfined: true, + 'image/png': { width: '999px' } + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); }); }); }); - }); - suite('Output Order', () => { - test('Verify order of outputs', async () => { - const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [ - { - output: { - data: { - 'application/vnd.vegalite.v4+json': 'some json', - 'text/html': 'Hello' + suite('Output Order', () => { + test('Verify order of outputs', async () => { + const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [ + { + output: { + data: { + 'application/vnd.vegalite.v4+json': 'some json', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html'] }, - expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html'] - }, - { - output: { - data: { - 'application/vnd.vegalite.v4+json': 'some json', - 'application/javascript': 'some js', - 'text/plain': 'some text', - 'text/html': 'Hello' + { + output: { + data: { + 'application/vnd.vegalite.v4+json': 'some json', + 'application/javascript': 'some js', + 'text/plain': 'some text', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: [ + 'application/vnd.vegalite.v4+json', + 'text/html', + 'application/javascript', + 'text/plain' + ] }, - expectedMimeTypesOrder: [ - 'application/vnd.vegalite.v4+json', - 'text/html', - 'application/javascript', - 'text/plain' - ] - }, - { - output: { - data: { - 'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes. - 'application/javascript': 'some js', - 'text/plain': 'some text', - 'text/html': 'Hello' + { + output: { + data: { + 'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes. + 'application/javascript': 'some js', + 'text/plain': 'some text', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: [ + 'text/html', + 'application/javascript', + 'text/plain', + 'application/vnd.vegalite.v4+json' + ] }, - expectedMimeTypesOrder: [ - 'text/html', - 'application/javascript', - 'text/plain', - 'application/vnd.vegalite.v4+json' - ] - }, - { - output: { - data: { - 'text/plain': 'some text', - 'text/html': 'Hello' + { + output: { + data: { + 'text/plain': 'some text', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: ['text/html', 'text/plain'] }, - expectedMimeTypesOrder: ['text/html', 'text/plain'] - }, - { - output: { - data: { - 'application/javascript': 'some js', - 'text/plain': 'some text' + { + output: { + data: { + 'application/javascript': 'some js', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: ['application/javascript', 'text/plain'] }, - expectedMimeTypesOrder: ['application/javascript', 'text/plain'] - }, - { - output: { - data: { - 'image/svg+xml': 'some svg', - 'text/plain': 'some text' + { + output: { + data: { + 'image/svg+xml': 'some svg', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: ['image/svg+xml', 'text/plain'] }, - expectedMimeTypesOrder: ['image/svg+xml', 'text/plain'] - }, - { - output: { - data: { - 'text/latex': 'some latex', - 'text/plain': 'some text' + { + output: { + data: { + 'text/latex': 'some latex', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: ['text/latex', 'text/plain'] }, - expectedMimeTypesOrder: ['text/latex', 'text/plain'] - }, - { - output: { - data: { - 'application/vnd.jupyter.widget-view+json': 'some widget', - 'text/plain': 'some text' + { + output: { + data: { + 'application/vnd.jupyter.widget-view+json': 'some widget', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' + expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain'] }, - expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain'] - }, - { - output: { - data: { - 'text/plain': 'some text', - 'image/svg+xml': 'some svg', - 'image/png': 'some png' + { + output: { + data: { + 'text/plain': 'some text', + 'image/svg+xml': 'some svg', + 'image/png': 'some png' + }, + metadata: {}, + output_type: 'display_data' }, - metadata: {}, - output_type: 'display_data' - }, - expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain'] - } - ]; + expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain'] + } + ]; - dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => { - const sortedOutputs = jupyterCellOutputToCellOutput(output); - const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(','); - assert.equal(mimeTypes, expectedMimeTypesOrder.join(',')); + dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => { + const sortedOutputs = jupyterCellOutputToCellOutput(output); + const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(','); + assert.equal(mimeTypes, expectedMimeTypesOrder.join(',')); + }); }); }); }); diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index 4029985233a..12f6e5cac1f 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -111,7 +111,7 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]].*$" + "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f7c87fbf9fa..f78f494d727 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; +import { Disposable, ExtensionContext, Uri, l10n, window } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable } from '../jsonClient'; +import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -43,7 +43,10 @@ export async function activate(context: ExtensionContext) { } }; - client = await startClient(context, newLanguageClient, { schemaRequests, timer }); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); + + client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel }); } catch (e) { console.log(e); diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index ce81dcb4c9e..f892664d917 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/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,16 @@ 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); + 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 +352,7 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa } }; - clientOptions.outputChannel = outputChannel; + clientOptions.outputChannel = runtime.logOutputChannel; // Create the language client and start the client. const client = newLanguageClient('json', languageServerDescription, clientOptions); client.registerProposedFeatures(); diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 79d66e32dda..d57ebf80834 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; +import { Disposable, ExtensionContext, LogOutputChannel, window, l10n, env, LogLevel } from 'vscode'; import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; @@ -14,15 +14,16 @@ import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-li import TelemetryReporter from '@vscode/extension-telemetry'; import { JSONSchemaCache } from './schemaCache'; -let telemetry: TelemetryReporter | undefined; let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { const clientPackageJSON = await getPackageInfo(context); - telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + const telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + context.subscriptions.push(telemetry); - const outputChannel = window.createOutputChannel(languageServerDescription); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/jsonServerMain`; const serverModule = context.asAbsolutePath(serverMain); @@ -38,11 +39,8 @@ export async function activate(context: ExtensionContext) { }; const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - clientOptions.outputChannel = outputChannel; return new LanguageClient(id, name, serverOptions, clientOptions); }; - const log = getLog(outputChannel); - context.subscriptions.push(log); const timer = { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { @@ -54,9 +52,9 @@ export async function activate(context: ExtensionContext) { // pass the location of the localization bundle to the server process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; - const schemaRequests = await getSchemaRequestService(context, log); + const schemaRequests = await getSchemaRequestService(context, logOutputChannel); - client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer }); + client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel }); } export async function deactivate(): Promise { @@ -64,7 +62,6 @@ export async function deactivate(): Promise { await client.dispose(); client = undefined; } - telemetry?.dispose(); } interface IPackageInfo { @@ -84,36 +81,9 @@ async function getPackageInfo(context: ExtensionContext): Promise } } -interface Log { - trace(message: string): void; - isTrace(): boolean; - dispose(): void; -} - -const traceSetting = 'json.trace.server'; -function getLog(outputChannel: OutputChannel): Log { - let trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - const configListener = workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(traceSetting)) { - trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - } - }); - return { - trace(message: string) { - if (trace) { - outputChannel.appendLine(message); - } - }, - isTrace() { - return trace; - }, - dispose: () => configListener.dispose() - }; -} - const retryTimeoutInHours = 2 * 24; // 2 days -async function getSchemaRequestService(context: ExtensionContext, log: Log): Promise { +async function getSchemaRequestService(context: ExtensionContext, log: LogOutputChannel): Promise { let cache: JSONSchemaCache | undefined = undefined; const globalStorage = context.globalStorageUri; @@ -191,7 +161,7 @@ async function getSchemaRequestService(context: ExtensionContext, log: Log): Pro if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) { const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours); if (content) { - if (log.isTrace()) { + if (log.logLevel === LogLevel.Trace) { log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`); } diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index bddc40acd34..7d50c250b0d 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -15,8 +15,8 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.2.1", "request-light": "^0.7.0", - "vscode-json-languageservice": "^5.3.9", - "vscode-languageserver": "^9.0.2-next.1", + "vscode-json-languageservice": "^5.3.10", + "vscode-languageserver": "^10.0.0-next.2", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index fc1159b9160..598fe822994 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -27,10 +27,10 @@ request-light@^0.7.0: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== -vscode-json-languageservice@^5.3.9: - version "5.3.9" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.3.9.tgz#512463ed580237d958df9280b43da9e3b5b621ce" - integrity sha512-0IcymTw0ZYX5Zcx+7KLLwTRvg0FzXUVnM1hrUH+sPhqEX0fHGg2h5UUOSp1f8ydGS7/xxzlFI3TR01yaHs6Y0Q== +vscode-json-languageservice@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.3.10.tgz#7d56872cbb7460baf0491cea31807e537244dbae" + integrity sha512-KlbUYaer3DAnsVyRtgg/MhXOu4TTwY8TjaZYRY7Mt80zSpmvbmd58YT4Wq2ZiqHzdioD6lAvRSxhSCL0DvVY8Q== dependencies: "@vscode/l10n" "^0.0.18" jsonc-parser "^3.2.1" @@ -38,40 +38,40 @@ vscode-json-languageservice@^5.3.9: vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@8.2.1-next.1: - version "8.2.1-next.1" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1-next.1.tgz#52e1091907b56759114fabac803b18c44a48f2a9" - integrity sha512-L+DYtdUtqUXGpyMgHqer6IBKvFFhl/1ToiMmCmG85LYHuuX0jllHMz77MYt0RicakoYY+Lq1yLK6Qj3YBqgzDQ== +vscode-jsonrpc@9.0.0-next.2: + version "9.0.0-next.2" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" + integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== -vscode-languageserver-protocol@3.17.6-next.1: - version "3.17.6-next.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.1.tgz#5d87f7f708667cf04dbefb5c860901df7d01ebc1" - integrity sha512-2npXUc8oe/fb9Bjcwm2HTWYZXyCbW4NTo7jkOrEciGO+/LfWbSMgqZ6PwKWgqUkgCbkPxQHNjoMqr9ol/Ehjgg== +vscode-languageserver-protocol@3.17.6-next.3: + version "3.17.6-next.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.3.tgz#09d3e28e9ad12270233d07fa0b69cf1d51d7dfe4" + integrity sha512-H8ATH5SAvc3JzttS+AL6g681PiBOZM/l34WP2JZk4akY3y7NqTP+f9cJ+MhrVBbD3aDS8bdAKewZgbFLW6M8Pg== dependencies: - vscode-jsonrpc "8.2.1-next.1" - vscode-languageserver-types "3.17.6-next.1" + vscode-jsonrpc "9.0.0-next.2" + vscode-languageserver-types "3.17.6-next.3" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== -vscode-languageserver-types@3.17.6-next.1: - version "3.17.6-next.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.1.tgz#a3d2006d52f7d4026ea67668113ec16c73cd8f1d" - integrity sha512-7xVc/xLtNhKuCKX0mINT6mFUrUuRz0EinhwPGT8Gtsv2hlo+xJb5NKbiGailcWa1/T5e4dr5Pb2MfGchHreHAA== +vscode-languageserver-types@3.17.6-next.3: + version "3.17.6-next.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" + integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== 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@^9.0.2-next.1: - version "9.0.2-next.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.2-next.1.tgz#cc9bbd66716346aa761e5bafa19d64559ab4e030" - integrity sha512-xySldxoHIcKXtxoI0LqRX3QcTdOVFt1SeHV0hyPq28p7xGPqWxUPcmTcfIqYdHefXG22nd8DQbGWOEe52yu08A== +vscode-languageserver@^10.0.0-next.2: + version "10.0.0-next.2" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.2.tgz#9a8ac58f72979961497c4fd7f6097561d4134d5f" + integrity sha512-WZdK/XO6EkNU6foYck49NpS35sahWhYFs4hwCGalH/6lhPmdUKABTnWioK/RLZKWqH8E5HdlAHQMfSBIxKBV9Q== dependencies: - vscode-languageserver-protocol "3.17.6-next.1" + vscode-languageserver-protocol "3.17.6-next.3" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 9f29ee2197a..df4818e025b 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -141,9 +141,9 @@ request-light@^0.7.0: integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== semver@^7.3.7: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index cd025113ad7..965df91bed4 100644 --- a/extensions/latex/cgmanifest.json +++ b/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/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index bf4ee5e89be..60c6b192bed 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/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/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index b5472c56cfd..c84c468b80c 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/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/extensions/markdown-language-features/media/highlight.css b/extensions/markdown-language-features/media/highlight.css index 47444a10348..6342ac12cd5 100644 --- a/extensions/markdown-language-features/media/highlight.css +++ b/extensions/markdown-language-features/media/highlight.css @@ -159,13 +159,13 @@ Visual Studio-like style based on original C# coloring by Jason Diamond { - 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); + // In some browsers, copying from the address bar sets both text/uri-list and text/plain. + // Disable ourselves if there's also a text entry with the same http(s) uri as our list, + // unless we are explicitly requested. + if ( + uriList.entries.length === 1 + && (uriList.entries[0].uri.scheme === Schemes.http || uriList.entries[0].uri.scheme === Schemes.https) + && !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 +198,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 +239,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/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index a57f0d39005..7fcff576a3d 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/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/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index 8bfc9ae2ff5..563c125cfc6 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/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/extensions/markdown-language-features/src/languageFeatures/copyFiles/smartDropOrPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/smartDropOrPaste.ts new file mode 100644 index 00000000000..deaa4b58212 --- /dev/null +++ b/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/extensions/markdown-language-features/src/test/pasteUrl.test.ts b/extensions/markdown-language-features/src/test/pasteUrl.test.ts index df863a25652..2afa4465f76 100644 --- a/extensions/markdown-language-features/src/test/pasteUrl.test.ts +++ b/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/extensions/markdown-language-features/src/util/uriList.ts b/extensions/markdown-language-features/src/util/uriList.ts index 04897af453e..8b7f52e568f 100644 --- a/extensions/markdown-language-features/src/util/uriList.ts +++ b/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/extensions/package.json b/extensions/package.json index 4365c20acc1..35b736f0443 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "5.3.2" + "typescript": "5.4.3" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/positron-python/src/client/activation/jedi/languageServerProxy.ts b/extensions/positron-python/src/client/activation/jedi/languageServerProxy.ts index d7ffe8328b9..cb6d401782a 100644 --- a/extensions/positron-python/src/client/activation/jedi/languageServerProxy.ts +++ b/extensions/positron-python/src/client/activation/jedi/languageServerProxy.ts @@ -41,6 +41,7 @@ export class JediLanguageServerProxy implements ILanguageServerProxy { undefined, true, undefined, + // @ts-ignore Flagged by Typescript 5.5-dev JediLanguageServerProxy.versionTelemetryProps, ) public async start( @@ -103,6 +104,7 @@ export class JediLanguageServerProxy implements ILanguageServerProxy { undefined, true, undefined, + // @ts-ignore Flagged by Typescript 5.5-dev JediLanguageServerProxy.versionTelemetryProps, ) private registerHandlers(client: LanguageClient) { diff --git a/extensions/positron-python/src/client/activation/jedi/manager.ts b/extensions/positron-python/src/client/activation/jedi/manager.ts index bafdcc735a1..a99890dff0f 100644 --- a/extensions/positron-python/src/client/activation/jedi/manager.ts +++ b/extensions/positron-python/src/client/activation/jedi/manager.ts @@ -118,6 +118,7 @@ export class JediLanguageServerManager implements ILanguageServerManager { undefined, true, undefined, + // @ts-ignore Flagged by Typescript 5.5-dev JediLanguageServerManager.versionTelemetryProps, ) @traceDecoratorVerbose('Starting language server') diff --git a/extensions/positron-python/src/client/activation/node/languageServerProxy.ts b/extensions/positron-python/src/client/activation/node/languageServerProxy.ts index 45d1d1a17fe..dae488037c0 100644 --- a/extensions/positron-python/src/client/activation/node/languageServerProxy.ts +++ b/extensions/positron-python/src/client/activation/node/languageServerProxy.ts @@ -86,6 +86,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { undefined, true, undefined, + // @ts-ignore Flagged by Typescript 5.5-dev NodeLanguageServerProxy.versionTelemetryProps, ) public async start( @@ -161,6 +162,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { undefined, true, undefined, + // @ts-ignore Flagged by Typescript 5.5-dev NodeLanguageServerProxy.versionTelemetryProps, ) private registerHandlers(client: LanguageClient, _resource: Resource) { diff --git a/extensions/positron-python/src/client/activation/node/manager.ts b/extensions/positron-python/src/client/activation/node/manager.ts index 5a66e4abecd..cf7419aa918 100644 --- a/extensions/positron-python/src/client/activation/node/manager.ts +++ b/extensions/positron-python/src/client/activation/node/manager.ts @@ -111,6 +111,7 @@ export class NodeLanguageServerManager implements ILanguageServerManager { undefined, true, undefined, + // @ts-ignore Flagged by Typescript 5.5-dev NodeLanguageServerManager.versionTelemetryProps, ) @traceDecoratorVerbose('Starting language server') diff --git a/extensions/razor/cgmanifest.json b/extensions/razor/cgmanifest.json index e90c7d75d8c..d3685974bdb 100644 --- a/extensions/razor/cgmanifest.json +++ b/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/extensions/razor/syntaxes/cshtml.tmLanguage.json b/extensions/razor/syntaxes/cshtml.tmLanguage.json index 4594037960a..389a6daf249 100644 --- a/extensions/razor/syntaxes/cshtml.tmLanguage.json +++ b/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/extensions/ruby/language-configuration.json b/extensions/ruby/language-configuration.json index a86f592e3bd..e1125e0bf2b 100644 --- a/extensions/ruby/language-configuration.json +++ b/extensions/ruby/language-configuration.json @@ -26,6 +26,6 @@ ], "indentationRules": { "increaseIndentPattern": "^\\s*((begin|class|(private|protected)\\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\\sdo\\b)|([^#]*=\\s*(case|if|unless)))\\b([^#\\{;]|(\"|'|\/).*\\4)*(#.*)?$", - "decreaseIndentPattern": "^\\s*([}\\]]([,)]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)|(end|rescue|ensure|else|elsif|when|in)\\b)" + "decreaseIndentPattern": "^\\s*([}\\]]([,)]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)|(end|rescue|ensure|else|elsif)\\b|(in|when)\\s)" } } diff --git a/extensions/scss/cgmanifest.json b/extensions/scss/cgmanifest.json index 12247769ce2..a67a4f54609 100644 --- a/extensions/scss/cgmanifest.json +++ b/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/extensions/shellscript/cgmanifest.json b/extensions/shellscript/cgmanifest.json index cedea83514c..d82c527ddf0 100644 --- a/extensions/shellscript/cgmanifest.json +++ b/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/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json index b4a2156e271..dbb06cba48e 100644 --- a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json +++ b/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": "(?\\s*)|((.*[^\\w]+|\\s*)(if|while|for)\\s*\\(.*\\)\\s*))$" } }, "onEnterRules": [ @@ -215,6 +218,33 @@ "action": { "indent": "outdent" } - } + }, + // Indent when pressing enter from inside () + { + "beforeText": "^.*\\([^\\)]*$", + "afterText": "^\\s*\\).*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside {} + { + "beforeText": "^.*\\{[^\\}]*$", + "afterText": "^\\s*\\}.*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside [] + { + "beforeText": "^.*\\[[^\\]]*$", + "afterText": "^\\s*\\].*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, ] } diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index c8c922b9d22..ce7ebf93e9d 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -45,7 +45,7 @@ "@vscode/ts-package-manager": "^0.0.2", "jsonc-parser": "^3.2.0", "semver": "7.5.2", - "vscode-tas-client": "^0.1.63", + "vscode-tas-client": "^0.1.84", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 42e7f9f7461..7fa9805a543 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -215,12 +215,16 @@ export default class FileConfigurationManager extends Disposable { private getAutoImportFileExcludePatternsPreference(config: vscode.WorkspaceConfiguration, workspaceFolder: vscode.Uri | undefined): string[] | undefined { return workspaceFolder && config.get('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/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index a627670c70a..f724cfd8c44 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/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/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index dc5948314d9..9f5d76f5ac3 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -725,6 +725,13 @@ export default class BufferSyncSupport extends Disposable { orderedFileSet.set(buffer.resource, undefined); } + for (const { resource } of orderedFileSet.entries()) { + const buffer = this.syncedBuffers.get(resource); + if (buffer && !this.shouldValidate(buffer)) { + orderedFileSet.delete(resource); + } + } + if (orderedFileSet.size) { const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, orderedFileSet, () => { if (this.pendingGetErr === getErr) { diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index b8d863ebb1a..45e09d63481 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/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/extensions/typescript-language-features/src/tsconfig.ts b/extensions/typescript-language-features/src/tsconfig.ts index 196cf185170..04f08a128bc 100644 --- a/extensions/typescript-language-features/src/tsconfig.ts +++ b/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/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 0796befc6bb..812a9c457bf 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/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/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 482239a3cbb..bc72fe4cb8b 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -140,46 +140,6 @@ resolved "https://registry.yarnpkg.com/@vscode/ts-package-manager/-/ts-package-manager-0.0.2.tgz#d1cade5ff0d01da8c5b5b00bf79d80e7156771cf" integrity sha512-cXPxGbPVTkEQI8mUiWYUwB6j3ga6M9i7yubUOCrjgZ01GeZPMSnaWRprfJ09uuy81wJjY2gfHgLsOgwrGvUBTw== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -axios@^1.6.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" - integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== - dependencies: - follow-redirects "^1.15.0" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -follow-redirects@^1.15.0: - version "1.15.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" - integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== - -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" @@ -192,23 +152,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - semver@7.5.2: version "7.5.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb" @@ -216,19 +159,17 @@ semver@7.5.2: dependencies: lru-cache "^6.0.0" -tas-client@0.1.73: - version "0.1.73" - resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.1.73.tgz#2dacf68547a37989ef1554c6510dc108a1ea7a71" - integrity sha512-UDdUF9kV2hYdlv+7AgqP2kXarVSUhjK7tg1BUflIRGEgND0/QoNpN64rcEuhEcM8AIbW65yrCopJWqRhLZ3m8w== - dependencies: - axios "^1.6.1" +tas-client@0.2.33: + version "0.2.33" + resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.2.33.tgz#451bf114a8a64748030ce4068ab7d079958402e6" + integrity sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg== -vscode-tas-client@^0.1.63: - version "0.1.75" - resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.75.tgz#771780a9a178163028299f52d41973300060dd38" - integrity sha512-/+ALFWPI4U3obeRvLFSt39guT7P9bZQrkmcLoiS+2HtzJ/7iPKNt5Sj+XTiitGlPYVFGFc0plxX8AAp6Uxs0xQ== +vscode-tas-client@^0.1.84: + version "0.1.84" + resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" + integrity sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w== dependencies: - tas-client "0.1.73" + tas-client "0.2.33" vscode-uri@3.0.3: version "3.0.3" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index e9323fc9c43..1c54b960a91 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -45,7 +45,6 @@ "terminalDataWriteEvent", "terminalDimensions", "tunnels", - "testCoverage", "testObserver", "textSearchProvider", "timeline", @@ -64,6 +63,20 @@ }, "icon": "media/icon.png", "contributes": { + "chatParticipants": [ + { + "id": "api-test.participant", + "name": "participant", + "description": "test", + "isDefault": true, + "commands": [ + { + "name": "hello", + "description": "Hello" + } + ] + } + ], "configuration": { "type": "object", "title": "Test Config", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 6fb0262e132..cd8614a86e7 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; +import { commands, CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; import { DeferredPromise, assertNoRpc, closeAllEditors, disposeAll } from '../utils'; suite('chat', () => { @@ -30,7 +30,7 @@ suite('chat', () => { function setupParticipant(): Event<{ request: ChatRequest; context: ChatContext }> { const emitter = new EventEmitter<{ request: ChatRequest; context: ChatContext }>(); - disposables.push(); + disposables.push(emitter); disposables.push(interactive.registerInteractiveSessionProvider('provider', { prepareSession: (_token: CancellationToken): ProviderResult => { return { @@ -40,37 +40,31 @@ suite('chat', () => { }, })); - const participant = chat.createChatParticipant('participant', (request, context, _progress, _token) => { + const participant = chat.createChatParticipant('api-test.participant', (request, context, _progress, _token) => { emitter.fire({ request, context }); - 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 () => { + test('participant and slash command history', 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 => { + disposables.push(onRequest(request => { if (i === 0) { 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'); + assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); assert.strictEqual(request.context.history[0].command, 'hello'); } - }); + })); }); test('participant and variable', async () => { @@ -81,7 +75,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'); @@ -98,24 +92,19 @@ suite('chat', () => { })); const deferred = new DeferredPromise(); - const participant = chat.createChatParticipant('participant', (_request, _context, _progress, _token) => { + const participant = chat.createChatParticipant('api-test.participant', (_request, _context, _progress, _token) => { 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/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts index c2cdd073d74..e2145d4ee28 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts +++ b/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/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts index 4990e30af59..f9d8d6a82db 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts +++ b/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/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index fe57d7a883c..8d193edcc91 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/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/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index 58297eb4e5b..37e16207ddb 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/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/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts index ba7ce21e32f..4f8331c286f 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts +++ b/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/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts index 4b007a918a3..64b6354ac9d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts @@ -36,7 +36,7 @@ suite('vscode API - globalState / workspaceState', () => { await state.update('state.test.get', undefined); keys = state.keys(); - assert.strictEqual(keys.length, 0); + assert.strictEqual(keys.length, 0, `Unexpected keys: ${JSON.stringify(keys)}`); res = state.get('state.test.get', 'default'); assert.strictEqual(res, 'default'); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 11caa87618d..2eb115761d1 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -934,7 +934,7 @@ suite('vscode API - workspace', () => { async function test77735(withOpenedEditor: boolean): Promise { const docUriOriginal = await createRandomFile(); const docUriMoved = docUriOriginal.with({ path: `${docUriOriginal.path}.moved` }); - await deleteFile(docUriMoved); // ensure target does not exist + await deleteFile(docUriMoved); if (withOpenedEditor) { const document = await vscode.workspace.openTextDocument(docUriOriginal); @@ -967,8 +967,9 @@ suite('vscode API - workspace', () => { const document = await vscode.workspace.openTextDocument(newUri); assert.strictEqual(document.isDirty, true); - await document.save(); - assert.strictEqual(document.isDirty, false); + const result = await document.save(); + assert.strictEqual(result, true, `save failed in iteration: ${i} (docUriOriginal: ${docUriOriginal.fsPath})`); + assert.strictEqual(document.isDirty, false, `document still dirty in iteration: ${i} (docUriOriginal: ${docUriOriginal.fsPath})`); assert.strictEqual(document.getText(), expected); diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json index c24aadf2ed9..5948ea77fcd 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json +++ b/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/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json index 198ace22005..ef48d804f66 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json +++ b/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/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json index 1d9a16304e1..da61966d5c0 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json +++ b/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/extensions/yaml/package.json b/extensions/yaml/package.json index 96e02e37da6..5223f71c52b 100644 --- a/extensions/yaml/package.json +++ b/extensions/yaml/package.json @@ -37,10 +37,10 @@ "yaml" ], "extensions": [ + ".yaml", ".yml", ".eyaml", ".eyml", - ".yaml", ".cff", ".yaml-tmlanguage", ".yaml-tmpreferences", diff --git a/extensions/yarn.lock b/extensions/yarn.lock index f4543f6a1c9..e40d6068fe5 100644 --- a/extensions/yarn.lock +++ b/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.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" + integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index 59f2ba57e08..ec4f580b13d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.87.0", - "distro": "b314654a31bdba8cd2b0c7548e931916d03416bf", + "version": "1.88.0", + "distro": "340432a8308f66007779ec2133ee39e6995006cb", "author": { "name": "Microsoft Corporation" }, @@ -84,14 +84,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.12", + "@xterm/addon-image": "0.8.0-beta.12", + "@xterm/addon-search": "0.15.0-beta.12", + "@xterm/addon-serialize": "0.13.0-beta.12", + "@xterm/addon-unicode11": "0.8.0-beta.12", + "@xterm/addon-webgl": "0.18.0-beta.12", + "@xterm/headless": "5.5.0-beta.12", + "@xterm/xterm": "5.5.0-beta.12", "graceful-fs": "4.2.11", "he": "^1.2.0", "http-proxy-agent": "^7.0.0", @@ -102,7 +102,7 @@ "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", "react": "^18.2.0", "react-dom": "^18.2.0", "react-window": "^1.8.8", @@ -112,7 +112,7 @@ "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" }, "devDependencies": { @@ -142,7 +142,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", @@ -164,7 +164,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "27.3.2", + "electron": "28.2.8", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", @@ -230,7 +230,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/product.json b/product.json index cd03fb199a7..e313c06b93c 100644 --- a/product.json +++ b/product.json @@ -54,8 +54,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.87.0", - "sha256": "155ba715cc1045835951e70a663010c8f91d773d90a3ec504a041fd53b5658b0", + "version": "1.88.0", + "sha256": "3a048d87c0fac116fce9190ff6042ec7691e66fc17211a058faf0db91bb6605e", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/remote/.yarnrc b/remote/.yarnrc index cac528fd2dd..4e7208cdf69 100644 --- a/remote/.yarnrc +++ b/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/remote/package.json b/remote/package.json index 1d341b00a20..974450e8fea 100644 --- a/remote/package.json +++ b/remote/package.json @@ -13,14 +13,14 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.12", + "@xterm/addon-image": "0.8.0-beta.12", + "@xterm/addon-search": "0.15.0-beta.12", + "@xterm/addon-serialize": "0.13.0-beta.12", + "@xterm/addon-unicode11": "0.8.0-beta.12", + "@xterm/addon-webgl": "0.18.0-beta.12", + "@xterm/headless": "5.5.0-beta.12", + "@xterm/xterm": "5.5.0-beta.12", "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" } } diff --git a/remote/web/package.json b/remote/web/package.json index 59d5d813b5f..a2457c24fc3 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,13 +7,13 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.7.0-beta.12", + "@xterm/addon-image": "0.8.0-beta.12", + "@xterm/addon-search": "0.15.0-beta.12", + "@xterm/addon-serialize": "0.13.0-beta.12", + "@xterm/addon-unicode11": "0.8.0-beta.12", + "@xterm/addon-webgl": "0.18.0-beta.12", + "@xterm/xterm": "5.5.0-beta.12", "he": "^1.2.0", "jschardet": "3.0.0", "react": "^18.2.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 7c13de66ca9..118a33af031 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -55,40 +55,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.12": + version "0.7.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.12.tgz#200f0293c507b75064963b5fc72a115799533920" + integrity sha512-euzQyWdklaSxzmb87kuwwiVP06vuYe1oUK+CiQW24UggSXThOEvZhvYV3O6iEgLe3p+7QfgnRWohXhCM84VOew== + +"@xterm/addon-image@0.8.0-beta.12": + version "0.8.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.12.tgz#3fc5cea489d7159bf496b3a6d6515a109dab7226" + integrity sha512-YsBhmzwxRmym2dUA2CSm52Wt3OLhydVHM+SZmRAJ0/hvfB7dDjtuXBUSIdQWB16WWbGdi4Iazcs/TTxtarX/yA== + +"@xterm/addon-search@0.15.0-beta.12": + version "0.15.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.12.tgz#0c54677512135bbf820e3949c9dabeacc690d495" + integrity sha512-63ZhxXj6jBYumVrWJ7ZssICSMz+jHsXbi67tDQNMwTRO/MJxTittZeTHQ7IQrRYzKQgixrX0rLH7AwrLBrn2uQ== + +"@xterm/addon-serialize@0.13.0-beta.12": + version "0.13.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.12.tgz#438c1a6249cf4da3773d3de11a56638fa88d752a" + integrity sha512-/32Gpcj37Ftqf6b4+H62rcB70jLXi9IQspod/2mK3K+Yza9X+Yc8VkAz8VgpKa6tzbh3Xk0XEo/dB6kVFv1Jsg== + +"@xterm/addon-unicode11@0.8.0-beta.12": + version "0.8.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.12.tgz#7066297e2c662f6c235a4814e337c5a7fc8a91e2" + integrity sha512-uNsWmRpl4LaBfykpP9CKMo+49gVxRxHoC5MFuMhqPPNhXShsdBii3YxglwoKtit1fwzVT0CIWEniZQMlGiTIuw== + +"@xterm/addon-webgl@0.18.0-beta.12": + version "0.18.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.12.tgz#410396fb6a3edd033eb16f334b88f31870952afb" + integrity sha512-wnIf5Xv0qAWQ0I1G5drKpEThA+D0f03iOTdtPR3uSLDfR8OsmpnSRgiR0Y0nAOnDmiCnDxu/wdBCKOAcXhWl2Q== + +"@xterm/xterm@5.5.0-beta.12": + version "5.5.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.12.tgz#a0f7445a10f958a4949fbe6989693c93e9e0265e" + integrity sha512-+I/vQh16ndYt8erj7zrxywPb+niyZC1W0H0w/ueDB3IPC7zPXxcETR0OGmglL7kq8Erb76ukBYXw9byXR2vtxg== he@^1.2.0: version "1.2.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index 5fdbd0988ea..d62eb4b548b 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -114,45 +114,45 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-canvas@0.7.0-beta.12": + version "0.7.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.12.tgz#200f0293c507b75064963b5fc72a115799533920" + integrity sha512-euzQyWdklaSxzmb87kuwwiVP06vuYe1oUK+CiQW24UggSXThOEvZhvYV3O6iEgLe3p+7QfgnRWohXhCM84VOew== + +"@xterm/addon-image@0.8.0-beta.12": + version "0.8.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.12.tgz#3fc5cea489d7159bf496b3a6d6515a109dab7226" + integrity sha512-YsBhmzwxRmym2dUA2CSm52Wt3OLhydVHM+SZmRAJ0/hvfB7dDjtuXBUSIdQWB16WWbGdi4Iazcs/TTxtarX/yA== + +"@xterm/addon-search@0.15.0-beta.12": + version "0.15.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.12.tgz#0c54677512135bbf820e3949c9dabeacc690d495" + integrity sha512-63ZhxXj6jBYumVrWJ7ZssICSMz+jHsXbi67tDQNMwTRO/MJxTittZeTHQ7IQrRYzKQgixrX0rLH7AwrLBrn2uQ== + +"@xterm/addon-serialize@0.13.0-beta.12": + version "0.13.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.12.tgz#438c1a6249cf4da3773d3de11a56638fa88d752a" + integrity sha512-/32Gpcj37Ftqf6b4+H62rcB70jLXi9IQspod/2mK3K+Yza9X+Yc8VkAz8VgpKa6tzbh3Xk0XEo/dB6kVFv1Jsg== + +"@xterm/addon-unicode11@0.8.0-beta.12": + version "0.8.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.12.tgz#7066297e2c662f6c235a4814e337c5a7fc8a91e2" + integrity sha512-uNsWmRpl4LaBfykpP9CKMo+49gVxRxHoC5MFuMhqPPNhXShsdBii3YxglwoKtit1fwzVT0CIWEniZQMlGiTIuw== + +"@xterm/addon-webgl@0.18.0-beta.12": + version "0.18.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.12.tgz#410396fb6a3edd033eb16f334b88f31870952afb" + integrity sha512-wnIf5Xv0qAWQ0I1G5drKpEThA+D0f03iOTdtPR3uSLDfR8OsmpnSRgiR0Y0nAOnDmiCnDxu/wdBCKOAcXhWl2Q== + +"@xterm/headless@5.5.0-beta.12": + version "5.5.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.12.tgz#5dc79a98d4653c37c388144ea2ab4fab664304f3" + integrity sha512-s1AS30MYb0KJ7sEruyywAi79lAjSgjVOasb6EOgOalaQBYWf5BY2HKBU+GOyRPFkusgEIBg0f/ID8uS1fiku9A== + +"@xterm/xterm@5.5.0-beta.12": + version "5.5.0-beta.12" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.12.tgz#a0f7445a10f958a4949fbe6989693c93e9e0265e" + integrity sha512-+I/vQh16ndYt8erj7zrxywPb+niyZC1W0H0w/ueDB3IPC7zPXxcETR0OGmglL7kq8Erb76ukBYXw9byXR2vtxg== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" @@ -431,10 +431,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" @@ -643,6 +643,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/resources/server/bin/code-server-linux.sh b/resources/server/bin/code-server-linux.sh index e3d96bdadf2..3df32dfd43c 100644 --- a/resources/server/bin/code-server-linux.sh +++ b/resources/server/bin/code-server-linux.sh @@ -9,24 +9,4 @@ esac ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")" -# 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 -# file before the server is downloaded and installed. -skip_check=0 -if [ -f "/tmp/vscode-skip-server-requirements-check" ]; then - echo "!!! WARNING: Skipping server pre-requisite check !!!" - echo "!!! Server stability is not guaranteed. Proceed at your own risk. !!!" - skip_check=1 -fi - -# Check platform requirements -if [ "$(echo "$@" | grep -c -- "--skip-requirements-check")" -eq 0 ] && [ $skip_check -eq 0 ]; then - $ROOT/bin/helpers/check-requirements.sh - exit_code=$? - if [ $exit_code -ne 0 ]; then - exit $exit_code - fi -fi - "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" diff --git a/resources/server/bin/helpers/check-requirements-linux-legacy.sh b/resources/server/bin/helpers/check-requirements-linux-legacy.sh new file mode 100755 index 00000000000..0db77676965 --- /dev/null +++ b/resources/server/bin/helpers/check-requirements-linux-legacy.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# + +set -e + +echo "!!! WARNING: Using legacy server, please check https://aka.ms/vscode-remote/faq/old-linux for additional information !!!" +exit 0 diff --git a/resources/server/bin/helpers/check-requirements-linux.sh b/resources/server/bin/helpers/check-requirements-linux.sh index be5207c2f05..079557869e3 100644 --- a/resources/server/bin/helpers/check-requirements-linux.sh +++ b/resources/server/bin/helpers/check-requirements-linux.sh @@ -5,14 +5,19 @@ 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 # file before the server is downloaded and installed. -# -# This check is duplicated between code-server-linux.sh and here -# since remote container calls into this script directly quite early -# before the usual server startup flow. if [ -f "/tmp/vscode-skip-server-requirements-check" ]; then echo "!!! WARNING: Skipping server pre-requisite check !!!" echo "!!! Server stability is not guaranteed. Proceed at your own risk. !!!" @@ -144,7 +149,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/resources/win32/bin/code.cmd b/resources/win32/bin/code.cmd index 9da8ab4f7b8..7e7b92c9eb7 100644 --- a/resources/win32/bin/code.cmd +++ b/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/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 82459f97178..ff113c9baa9 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,13 +921,6 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node): void { - const window = getWindow(element); - if (!window.document.hasFocus()) { - window.focus(); - } -} - const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/src/vs/base/browser/fonts.ts b/src/vs/base/browser/fonts.ts new file mode 100644 index 00000000000..a5e78d00bef --- /dev/null +++ b/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/src/vs/base/browser/keyboardEvent.ts b/src/vs/base/browser/keyboardEvent.ts index 57ba7407845..6aa5bf530f3 100644 --- a/src/vs/base/browser/keyboardEvent.ts +++ b/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/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index 79e3211d85f..db93de17f58 100644 --- a/src/vs/base/browser/touch.ts +++ b/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/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index fd7424b0f72..df6ec99660c 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/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/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 5f5ad352842..05505b768a5 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/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/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index 7094cc9c41e..b84a3d8a8b3 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -57,6 +57,7 @@ export class BreadcrumbsWidget { private _focusedItemIdx: number = -1; private _selectedItemIdx: number = -1; + private _pendingDimLayout: IDisposable | undefined; private _pendingLayout: IDisposable | undefined; private _dimension: dom.Dimension | undefined; @@ -100,6 +101,7 @@ export class BreadcrumbsWidget { dispose(): void { this._disposables.dispose(); this._pendingLayout?.dispose(); + this._pendingDimLayout?.dispose(); this._onDidSelectItem.dispose(); this._onDidFocusItem.dispose(); this._onDidChangeFocus.dispose(); @@ -112,11 +114,12 @@ export class BreadcrumbsWidget { if (dim && dom.Dimension.equals(dim, this._dimension)) { return; } - this._pendingLayout?.dispose(); if (dim) { // only measure - this._pendingLayout = this._updateDimensions(dim); + this._pendingDimLayout?.dispose(); + this._pendingDimLayout = this._updateDimensions(dim); } else { + this._pendingLayout?.dispose(); this._pendingLayout = this._updateScrollbar(); } } diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 4675ee8c5ee..1d2a3364d0b 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -9,8 +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 { 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'; @@ -29,6 +30,7 @@ export interface IButtonOptions extends Partial { readonly supportIcons?: boolean; readonly supportShortLabel?: boolean; readonly secondary?: boolean; + readonly hoverDelegate?: IHoverDelegate; } export interface IButtonStyles { @@ -115,6 +117,10 @@ export class Button extends Disposable implements IButton { this._element.classList.add('monaco-text-button-with-short-label'); } + if (typeof options.title === 'string') { + this.setTitle(options.title); + } + if (typeof options.ariaLabel === 'string') { this._element.setAttribute('aria-label', options.ariaLabel); } @@ -249,16 +255,13 @@ export class Button extends Disposable implements IButton { } else if (this.options.title) { title = renderStringAsPlaintext(value); } - if (!this._hover) { - this._hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._element, title)); - } else { - this._hover.update(title); - } + + this.setTitle(title); if (typeof this.options.ariaLabel === 'string') { this._element.setAttribute('aria-label', this.options.ariaLabel); } else if (this.options.ariaLabel) { - this._element.setAttribute('aria-label', this._element.title); + this._element.setAttribute('aria-label', title); } this._label = value; @@ -299,6 +302,14 @@ export class Button extends Disposable implements IButton { return !this._element.classList.contains('disabled'); } + private setTitle(title: string) { + if (!this._hover && title !== '') { + this._hover = this._register(setupCustomHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); + } else if (this._hover) { + this._hover.update(title); + } + } + focus(): void { this._element.focus(); } diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index de74fdbf744..48c0e2f1c46 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index af49847a810..a7debba4e6c 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/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/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 88dfaf2c5b1..dfc2329510f 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/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/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 419658a21bb..75333985372 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/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; @@ -93,7 +93,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.element.setAttribute('aria-haspopup', 'true'); this.element.setAttribute('aria-expanded', 'false'); if (this._action.label) { - this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.element, this._action.label)); + this._register(setupCustomHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.element, this._action.label)); } this.element.ariaLabel = this._action.label || ''; diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index 76af849c278..9ecfcbce827 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -16,6 +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 { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface IFindInputOptions { @@ -113,10 +114,13 @@ export class FindInput extends Widget { inputBoxStyles: options.inputBoxStyles, })); + const hoverDelegate = this._register(createInstantHoverDelegate()); + if (this.showCommonFindToggles) { this.regex = this._register(new RegexToggle({ appendTitle: appendRegexLabel, isChecked: false, + hoverDelegate, ...options.toggleStyles })); this._register(this.regex.onChange(viaKeyboard => { @@ -133,6 +137,7 @@ export class FindInput extends Widget { this.wholeWords = this._register(new WholeWordsToggle({ appendTitle: appendWholeWordsLabel, isChecked: false, + hoverDelegate, ...options.toggleStyles })); this._register(this.wholeWords.onChange(viaKeyboard => { @@ -146,6 +151,7 @@ export class FindInput extends Widget { this.caseSensitive = this._register(new CaseSensitiveToggle({ appendTitle: appendCaseSensitiveLabel, isChecked: false, + hoverDelegate, ...options.toggleStyles })); this._register(this.caseSensitive.onChange(viaKeyboard => { diff --git a/src/vs/base/browser/ui/findinput/findInputToggles.ts b/src/vs/base/browser/ui/findinput/findInputToggles.ts index 591ab981577..adce009430b 100644 --- a/src/vs/base/browser/ui/findinput/findInputToggles.ts +++ b/src/vs/base/browser/ui/findinput/findInputToggles.ts @@ -3,6 +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/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'; @@ -13,6 +15,7 @@ export interface IFindInputToggleOpts { readonly inputActiveOptionBorder: string | undefined; readonly inputActiveOptionForeground: string | undefined; readonly inputActiveOptionBackground: string | undefined; + readonly hoverDelegate?: IHoverDelegate; } const NLS_CASE_SENSITIVE_TOGGLE_LABEL = nls.localize('caseDescription', "Match Case"); @@ -25,6 +28,7 @@ export class CaseSensitiveToggle extends Toggle { icon: Codicon.caseSensitive, title: NLS_CASE_SENSITIVE_TOGGLE_LABEL + opts.appendTitle, isChecked: opts.isChecked, + hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, inputActiveOptionBackground: opts.inputActiveOptionBackground @@ -38,6 +42,7 @@ export class WholeWordsToggle extends Toggle { icon: Codicon.wholeWord, title: NLS_WHOLE_WORD_TOGGLE_LABEL + opts.appendTitle, isChecked: opts.isChecked, + hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, inputActiveOptionBackground: opts.inputActiveOptionBackground @@ -51,6 +56,7 @@ export class RegexToggle extends Toggle { icon: Codicon.regex, title: NLS_REGEX_TOGGLE_LABEL + opts.appendTitle, isChecked: opts.isChecked, + hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, inputActiveOptionBackground: opts.inputActiveOptionBackground diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts index 6cd7d4fb1c6..4dfdf549a3b 100644 --- a/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -16,6 +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/hoverDelegateFactory'; export interface IReplaceInputOptions { @@ -44,9 +45,10 @@ class PreserveCaseToggle extends Toggle { icon: Codicon.preserveCase, title: NLS_PRESERVE_CASE_LABEL + opts.appendTitle, isChecked: opts.isChecked, + hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, - inputActiveOptionBackground: opts.inputActiveOptionBackground + inputActiveOptionBackground: opts.inputActiveOptionBackground, }); } } diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index c2b41545d79..4847da97d2f 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/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/src/vs/base/browser/ui/hover/hoverDelegate.ts b/src/vs/base/browser/ui/hover/hoverDelegate.ts index 6682d739c26..57f6962ac0e 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate.ts +++ b/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: 'mouse' | '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/src/vs/base/browser/ui/hover/hoverDelegateFactory.ts b/src/vs/base/browser/ui/hover/hoverDelegateFactory.ts new file mode 100644 index 00000000000..44628261333 --- /dev/null +++ b/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/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hoverWidget.css similarity index 100% rename from src/vs/base/browser/ui/hover/hover.css rename to src/vs/base/browser/ui/hover/hoverWidget.css diff --git a/src/vs/base/browser/ui/hover/hoverWidget.ts b/src/vs/base/browser/ui/hover/hoverWidget.ts index dc0af66ff5a..bff397303be 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.ts +++ b/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/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts similarity index 85% rename from src/vs/base/browser/ui/iconLabel/iconLabelHover.ts rename to src/vs/base/browser/ui/hover/updatableHoverWidget.ts index 20ad4662b0d..d36ebab0d95 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts +++ b/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 { @@ -163,9 +166,24 @@ class UpdatableHoverWidget implements IDisposable { } } +function getHoverTargetElement(element: HTMLElement, stopElement?: HTMLElement): HTMLElement { + stopElement = stopElement ?? dom.getWindow(element).document.body; + while (!element.hasAttribute('custom-hover') && element !== stopElement) { + element = element.parentElement!; + } + return element; +} + export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IHoverContentOrFactory, options?: IUpdatableHoverOptions): ICustomHover { - let hoverPreparation: IDisposable | undefined; + htmlElement.setAttribute('custom-hover', 'true'); + + if (htmlElement.title !== '') { + console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.'); + console.trace('Stack trace:', htmlElement.title); + htmlElement.title = ''; + } + let hoverPreparation: IDisposable | undefined; let hoverWidget: UpdatableHoverWidget | undefined; const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => { @@ -206,7 +224,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM hideHover(false, (e).fromElement === htmlElement); }, true); - const onMouseOver = () => { + const onMouseOver = (e: MouseEvent) => { if (hoverPreparation) { return; } @@ -221,15 +239,20 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM // track the mouse position const onMouseMove = (e: MouseEvent) => { target.x = e.x + 10; - if ((e.target instanceof HTMLElement) && e.target.classList.contains('action-label')) { + if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target, htmlElement) !== htmlElement) { hideHover(true, true); } }; toDispose.add(dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_MOVE, onMouseMove, true)); } - toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); hoverPreparation = toDispose; + + if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target as HTMLElement, htmlElement) !== htmlElement) { + return; // Do not show hover when the mouse is over another hover target + } + + toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); }; const mouseOverDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_OVER, onMouseOver, true); @@ -247,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 @@ -265,7 +295,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); mouseUpEmitter.dispose(); - focusDomEmitter.dispose(); + focusDomEmitter?.dispose(); hideHover(true, true); } }; diff --git a/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts b/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts deleted file mode 100644 index f0209856dc3..00000000000 --- a/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/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index c9d62a9bc4e..c0e0544a93b 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/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/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts b/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts index 659572d4ff8..dc35bd8a9ab 100644 --- a/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts +++ b/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/src/vs/base/browser/ui/icons/iconSelectBox.ts b/src/vs/base/browser/ui/icons/iconSelectBox.ts index 465c1dc1181..b59529ffd81 100644 --- a/src/vs/base/browser/ui/icons/iconSelectBox.ts +++ b/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/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e4c89dd3aff..b899215c98f 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -11,6 +11,8 @@ import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Widget } from 'vs/base/browser/ui/widget'; import { IAction } from 'vs/base/common/actions'; @@ -111,6 +113,7 @@ export class InputBox extends Widget { private cachedContentHeight: number | undefined; private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; + private hover: ICustomHover | undefined; private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -230,7 +233,11 @@ export class InputBox extends Widget { public setTooltip(tooltip: string): void { this.tooltip = tooltip; - this.input.title = tooltip; + if (!this.hover) { + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); + } else { + this.hover.update(tooltip); + } } public setAriaLabel(label: string): void { @@ -305,6 +312,18 @@ export class InputBox extends Widget { return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd; } + public getSelection(): IRange | null { + const selectionStart = this.input.selectionStart; + if (selectionStart === null) { + return null; + } + const selectionEnd = this.input.selectionEnd ?? selectionStart; + return { + start: selectionStart, + end: selectionEnd, + }; + } + public enable(): void { this.input.removeAttribute('disabled'); } diff --git a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts index 431e33048cd..20eea5d8850 100644 --- a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts +++ b/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/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 0175a15779d..2ff770688d0 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -81,7 +81,7 @@ class PagedAccessibilityProvider implements IListAccessibilityProvider implements IListView { if (item.row) { item.row.domNode.style.height = ''; item.size = item.row.domNode.offsetHeight; + if (item.size === 0 && !isAncestor(item.row.domNode, getWindow(item.row.domNode).document.body)) { + console.warn('Measuring item node that is not in DOM! Add ListView to the DOM before measuring row height!'); + } item.lastDynamicHeightWidth = this.renderWidth; return item.size - size; } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 1a72e4e64d0..d467101fdee 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -28,6 +28,7 @@ import 'vs/css!./list'; import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError } from './list'; import { IListView, IListViewAccessibilityProvider, IListViewDragAndDrop, IListViewOptions, IListViewOptionsUpdate, ListViewTargetSector, ListView } from './listView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { autorun, constObservable, IObservable } from 'vs/base/common/observable'; interface ITraitChangeEvent { indexes: number[]; @@ -36,6 +37,11 @@ interface ITraitChangeEvent { type ITraitTemplateData = HTMLElement; +type IAccessibilityTemplateData = { + container: HTMLElement; + disposables: DisposableStore; +}; + interface IRenderedContainer { templateData: ITraitTemplateData; index: number; @@ -525,8 +531,11 @@ class TypeNavigationController implements IDisposable { // List: re-announce element on typing end since typed keys will interrupt aria label of focused element // Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961 const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0])); - if (ariaLabel) { + + if (typeof ariaLabel === 'string') { alert(ariaLabel); + } else if (ariaLabel) { + alert(ariaLabel.get()); } } this.previouslyFocused = -1; @@ -848,7 +857,7 @@ export interface IStyleController { } export interface IListAccessibilityProvider extends IListViewAccessibilityProvider { - getAriaLabel(element: T): string | null; + getAriaLabel(element: T): string | IObservable | null; getWidgetAriaLabel(): string; getWidgetRole?(): AriaRole; getAriaLevel?(element: T): number | undefined; @@ -1254,36 +1263,47 @@ class PipelineRenderer implements IListRenderer { } } -class AccessibiltyRenderer implements IListRenderer { +class AccessibiltyRenderer implements IListRenderer { templateId: string = 'a18n'; constructor(private accessibilityProvider: IListAccessibilityProvider) { } - renderTemplate(container: HTMLElement): HTMLElement { - return container; + renderTemplate(container: HTMLElement): IAccessibilityTemplateData { + return { container, disposables: new DisposableStore() }; } - renderElement(element: T, index: number, container: HTMLElement): void { + renderElement(element: T, index: number, data: IAccessibilityTemplateData): void { const ariaLabel = this.accessibilityProvider.getAriaLabel(element); + const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel); - if (ariaLabel) { - container.setAttribute('aria-label', ariaLabel); - } else { - container.removeAttribute('aria-label'); - } + data.disposables.add(autorun(reader => { + this.setAriaLabel(reader.readObservable(observable), data.container); + })); const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element); if (typeof ariaLevel === 'number') { - container.setAttribute('aria-level', `${ariaLevel}`); + data.container.setAttribute('aria-level', `${ariaLevel}`); } else { - container.removeAttribute('aria-level'); + data.container.removeAttribute('aria-level'); } } + private setAriaLabel(ariaLabel: string | null, element: HTMLElement): void { + if (ariaLabel) { + element.setAttribute('aria-label', ariaLabel); + } else { + element.removeAttribute('aria-label'); + } + } + + disposeElement(element: T, index: number, templateData: IAccessibilityTemplateData, height: number | undefined): void { + templateData.disposables.clear(); + } + disposeTemplate(templateData: any): void { - // noop + templateData.disposables.dispose(); } } @@ -1445,7 +1465,7 @@ export class List implements ISpliceable, IDisposable { const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list'; this.selection = new SelectionTrait(role !== 'listbox'); - const baseRenderers: IListRenderer[] = [this.focus.renderer, this.selection.renderer]; + const baseRenderers: IListRenderer[] = [this.focus.renderer, this.selection.renderer]; this.accessibilityProvider = _options.accessibilityProvider; diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 4429e37687d..41b406dc254 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/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'; @@ -30,11 +31,21 @@ export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g; -export enum Direction { +export enum HorizontalDirection { Right, Left } +export enum VerticalDirection { + Above, + Below +} + +export interface IMenuDirection { + horizontal: HorizontalDirection; + vertical: VerticalDirection; +} + export interface IMenuOptions { context?: unknown; actionViewItemProvider?: IActionViewItemProvider; @@ -43,7 +54,7 @@ export interface IMenuOptions { ariaLabel?: string; enableMnemonics?: boolean; anchorAlignment?: AnchorAlignment; - expandDirection?: Direction; + expandDirection?: IMenuDirection; useEventAsContext?: boolean; submenuIds?: Set; } @@ -724,7 +735,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { private mouseOver: boolean = false; private showScheduler: RunOnceScheduler; private hideScheduler: RunOnceScheduler; - private expandDirection: Direction; + private expandDirection: IMenuDirection; constructor( action: IAction, @@ -735,7 +746,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { ) { super(action, action, submenuOptions, menuStyles); - this.expandDirection = submenuOptions && submenuOptions.expandDirection !== undefined ? submenuOptions.expandDirection : Direction.Right; + this.expandDirection = submenuOptions && submenuOptions.expandDirection !== undefined ? submenuOptions.expandDirection : { horizontal: HorizontalDirection.Right, vertical: VerticalDirection.Below }; this.showScheduler = new RunOnceScheduler(() => { if (this.mouseOver) { @@ -849,11 +860,11 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { } } - private calculateSubmenuMenuLayout(windowDimensions: Dimension, submenu: Dimension, entry: IDomNodePagePosition, expandDirection: Direction): { top: number; left: number } { + private calculateSubmenuMenuLayout(windowDimensions: Dimension, submenu: Dimension, entry: IDomNodePagePosition, expandDirection: IMenuDirection): { top: number; left: number } { const ret = { top: 0, left: 0 }; // Start with horizontal - ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection === Direction.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }); + ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }); // We don't have enough room to layout the menu fully, so we are overlapping the menu if (ret.left >= entry.left && ret.left < entry.left + entry.width) { diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index feadeb47651..961fb8862a8 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -8,7 +8,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; -import { cleanMnemonic, Direction, IMenuOptions, IMenuStyles, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX } from 'vs/base/browser/ui/menu/menu'; +import { cleanMnemonic, HorizontalDirection, IMenuDirection, IMenuOptions, IMenuStyles, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, VerticalDirection } from 'vs/base/browser/ui/menu/menu'; import { ActionRunner, IAction, IActionRunner, Separator, SubmenuAction } from 'vs/base/common/actions'; import { asArray } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -32,7 +32,7 @@ export interface IMenuBarOptions { visibility?: string; getKeybinding?: (action: IAction) => ResolvedKeybinding | undefined; alwaysOnMnemonics?: boolean; - compactMode?: Direction; + compactMode?: IMenuDirection; actionRunner?: IActionRunner; getCompactMenuActions?: () => IAction[]; } @@ -333,9 +333,9 @@ export class MenuBar extends Disposable { } else { triggerKeys.push(KeyCode.Space); - if (this.options.compactMode === Direction.Right) { + if (this.options.compactMode?.horizontal === HorizontalDirection.Right) { triggerKeys.push(KeyCode.RightArrow); - } else if (this.options.compactMode === Direction.Left) { + } else if (this.options.compactMode?.horizontal === HorizontalDirection.Left) { triggerKeys.push(KeyCode.LeftArrow); } } @@ -1007,18 +1007,25 @@ export class MenuBar extends Disposable { const titleBoundingRect = customMenu.titleElement.getBoundingClientRect(); const titleBoundingRectZoom = DOM.getDomNodeZoomLevel(customMenu.titleElement); - if (this.options.compactMode === Direction.Right) { - menuHolder.style.top = `${titleBoundingRect.top}px`; + if (this.options.compactMode?.horizontal === HorizontalDirection.Right) { menuHolder.style.left = `${titleBoundingRect.left + this.container.clientWidth}px`; - } else if (this.options.compactMode === Direction.Left) { + } else if (this.options.compactMode?.horizontal === HorizontalDirection.Left) { menuHolder.style.top = `${titleBoundingRect.top}px`; menuHolder.style.right = `${this.container.clientWidth}px`; menuHolder.style.left = 'auto'; } else { - menuHolder.style.top = `${titleBoundingRect.bottom * titleBoundingRectZoom}px`; menuHolder.style.left = `${titleBoundingRect.left * titleBoundingRectZoom}px`; } + if (this.options.compactMode?.vertical === VerticalDirection.Above) { + // TODO@benibenj Do not hardcode the height of the menu holder + menuHolder.style.top = `${titleBoundingRect.top - this.menus.length * 30 + this.container.clientHeight}px`; + } else if (this.options.compactMode?.vertical === VerticalDirection.Below) { + menuHolder.style.top = `${titleBoundingRect.top}px`; + } else { + menuHolder.style.top = `${titleBoundingRect.bottom * titleBoundingRectZoom}px`; + } + customMenu.buttonElement.appendChild(menuHolder); const menuOptions: IMenuOptions = { @@ -1026,7 +1033,7 @@ export class MenuBar extends Disposable { actionRunner: this.actionRunner, enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics), ariaLabel: customMenu.buttonElement.getAttribute('aria-label') ?? undefined, - expandDirection: this.isCompact ? this.options.compactMode : Direction.Right, + expandDirection: this.isCompact ? this.options.compactMode : { horizontal: HorizontalDirection.Right, vertical: VerticalDirection.Below }, useEventAsContext: true }; diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 83e2d27eef7..be9064254c1 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/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/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index ca6aaa33505..c813532f496 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/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/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 536fb25608e..38192d39413 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/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/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index 54b9130d9e4..9e70dbdbe6f 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -13,8 +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 { 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; @@ -22,6 +23,7 @@ export interface IToggleOpts extends IToggleStyles { readonly title: string; readonly isChecked: boolean; readonly notFocusable?: boolean; + readonly hoverDelegate?: IHoverDelegate; } export interface IToggleStyles { @@ -57,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)); } @@ -130,7 +133,7 @@ export class Toggle extends Widget { } this.domNode = document.createElement('div'); - this._hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); + this._hover = this._register(setupCustomHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); this.domNode.classList.add(...classes); if (!this._opts.notFocusable) { this.domNode.tabIndex = 0; @@ -234,7 +237,7 @@ export class Checkbox extends Widget { constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) { super(); - this.checkbox = new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles }); + this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles })); this.domNode = this.checkbox.domNode; diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index f92369cddfb..ebb6bc264d1 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/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/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 16f985264d4..f3fc154141c 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -33,6 +33,9 @@ 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/hover/hoverDelegate'; +import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { autorun, constObservable } from 'vs/base/common/observable'; class TreeElementsDragAndDropData extends ElementsDragAndDropData { @@ -679,6 +682,7 @@ export interface ITreeFindToggleOpts { readonly inputActiveOptionBorder: string | undefined; readonly inputActiveOptionForeground: string | undefined; readonly inputActiveOptionBackground: string | undefined; + readonly hoverDelegate?: IHoverDelegate; } export class ModeToggle extends Toggle { @@ -687,6 +691,7 @@ export class ModeToggle extends Toggle { icon: Codicon.listFilter, title: localize('filter', "Filter"), isChecked: opts.isChecked ?? false, + hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, inputActiveOptionBackground: opts.inputActiveOptionBackground @@ -700,6 +705,7 @@ export class FuzzyToggle extends Toggle { icon: Codicon.searchFuzzy, title: localize('fuzzySearch', "Fuzzy Match"), isChecked: opts.isChecked ?? false, + hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, inputActiveOptionBackground: opts.inputActiveOptionBackground @@ -802,8 +808,9 @@ class FindWidget extends Disposable { this.elements.root.style.boxShadow = `0 0 8px 2px ${styles.listFilterWidgetShadow}`; } - this.modeToggle = this._register(new ModeToggle({ ...styles.toggleStyles, isChecked: mode === TreeFindMode.Filter })); - this.matchTypeToggle = this._register(new FuzzyToggle({ ...styles.toggleStyles, isChecked: matchType === TreeFindMatchType.Fuzzy })); + 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); this.onDidChangeMatchType = Event.map(this.matchTypeToggle.onChange, () => this.matchTypeToggle.checked ? TreeFindMatchType.Fuzzy : TreeFindMatchType.Contiguous, this._store); @@ -1657,8 +1664,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'); @@ -1666,7 +1679,7 @@ class StickyScrollWidget implements IDisposable { stickyElement.setAttribute('data-index', `${nodeIndex}`); stickyElement.setAttribute('data-parity', nodeIndex % 2 === 0 ? 'even' : 'odd'); stickyElement.setAttribute('id', this.view.getElementID(nodeIndex)); - this.setAccessibilityAttributes(stickyElement, stickyNode.node.element, stickyIndex, stickyNodesTotal); + const accessibilityDisposable = this.setAccessibilityAttributes(stickyElement, stickyNode.node.element, stickyIndex, stickyNodesTotal); // Get the renderer for the node const nodeTemplateId = this.treeDelegate.getTemplateId(stickyNode.node); @@ -1688,6 +1701,7 @@ class StickyScrollWidget implements IDisposable { // Remove the element from the DOM when state is disposed const disposable = toDisposable(() => { + accessibilityDisposable.dispose(); renderer.disposeElement(nodeCopy, stickyNode.startIndex, templateData, stickyNode.height); renderer.disposeTemplate(templateData); stickyElement.remove(); @@ -1696,9 +1710,9 @@ class StickyScrollWidget implements IDisposable { return { element: stickyElement, disposable }; } - private setAccessibilityAttributes(container: HTMLElement, element: T, stickyIndex: number, stickyNodesTotal: number): void { + private setAccessibilityAttributes(container: HTMLElement, element: T, stickyIndex: number, stickyNodesTotal: number): IDisposable { if (!this.accessibilityProvider) { - return; + return Disposable.None; } if (this.accessibilityProvider.getSetSize) { @@ -1712,8 +1726,20 @@ class StickyScrollWidget implements IDisposable { } const ariaLabel = this.accessibilityProvider.getAriaLabel(element); - if (ariaLabel) { - container.setAttribute('aria-label', ariaLabel); + const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel); + const result = autorun(reader => { + const value = reader.readObservable(observable); + + if (value) { + container.setAttribute('aria-label', value); + } else { + container.removeAttribute('aria-label'); + } + }); + + if (typeof ariaLabel === 'string') { + } else if (ariaLabel) { + container.setAttribute('aria-label', ariaLabel.get()); } const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element); @@ -1723,6 +1749,8 @@ class StickyScrollWidget implements IDisposable { // Sticky Scroll elements can not be selected container.setAttribute('aria-selected', String(false)); + + return result; } private setVisible(visible: boolean): void { @@ -1968,11 +1996,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); } @@ -1982,6 +2030,7 @@ class StickyScrollFocus extends Disposable { } this.domHasFocus = true; this.toggleStickyScrollFocused(true); + this.toggleCurrentElementActiveFocus(true); if (this.focusedIndex === -1) { this.setFocus(0); } @@ -1989,6 +2038,7 @@ class StickyScrollFocus extends Disposable { private onBlur(): void { this.domHasFocus = false; + this.toggleCurrentElementActiveFocus(false); } override dispose(): void { @@ -2048,6 +2098,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; @@ -2432,6 +2483,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); } @@ -2741,6 +2794,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; }`); } @@ -2870,27 +2925,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/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index 4e83338804b..219b7c143f1 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/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/src/vs/base/browser/window.ts b/src/vs/base/browser/window.ts index fe715d6f2c2..ab920e18349 100644 --- a/src/vs/base/browser/window.ts +++ b/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/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 9b510f82251..f8804af0fe7 100644 --- a/src/vs/base/common/arrays.ts +++ b/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/src/vs/base/common/cache.ts b/src/vs/base/common/cache.ts index 1e675c36e43..844a86b2584 100644 --- a/src/vs/base/common/cache.ts +++ b/src/vs/base/common/cache.ts @@ -39,17 +39,19 @@ export class Cache { /** * Uses a LRU cache to make a given parametrized function cached. * Caches just the last value. - * The key must be JSON serializable. */ export class LRUCachedFunction { private lastCache: TComputed | undefined = undefined; - private lastArgKey: string | undefined = undefined; + private lastArgKey: unknown | undefined = undefined; - constructor(private readonly fn: (arg: TArg) => TComputed) { + constructor( + private readonly fn: (arg: TArg) => TComputed, + private readonly _computeKey: (arg: TArg) => unknown = JSON.stringify, + ) { } public get(arg: TArg): TComputed { - const key = JSON.stringify(arg); + const key = this._computeKey(arg); if (this.lastArgKey !== key) { this.lastArgKey = key; this.lastCache = this.fn(arg); diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 688c7922fbe..6919e4934a2 100644 --- a/src/vs/base/common/codicons.ts +++ b/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,673 +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), - - // --- Start Positron --- - // Custom Codicons for Positron. In order to avoid namespace collisions, these are prefixed with 'positron-'. - positronNew: register('positron-new', 0xf230), - positronOpen: register('positron-open', 0xf231), - positronSave: register('positron-save', 0xf232), - positronSaveAll: register('positron-save-all', 0xf233), - positronPrint: register('positron-print', 0xf234), - positronDropDownArrow: register('positron-drop-down-arrow', 0xf235), - positronLogo: register('positron-logo', 0xf236), - positronPositLogo: register('positron-posit-logo', 0xf237), - positronSeparator: register('positron-separator', 0xf238), - positronVariablesView: register('positron-variables-view', 0xf239), - positronHelpView: register('positron-help-view', 0xf23a), - positronOutlineView: register('positron-outline-view', 0xf23b), - positronPlotView: register('positron-plot-view', 0xf23c), - positronPreviewView: register('positron-preview-view', 0xf23d), - positronLeftArrow: register('positron-left-arrow', 0xf23e), - positronRightArrow: register('positron-right-arrow', 0xf23f), - positronHome: register('positron-home', 0xf240), - positronRefresh: register('positron-refresh', 0xf241), - positronOpenInNewWindow: register('positron-open-in-new-window', 0xf242), - positronSearchIcon: register('positron-search-icon', 0xf243), - positronSearchCancel: register('positron-search-cancel', 0xf244), - positronShowTraceback: register('positron-show-traceback', 0xf245), - positronNewFolderFromGit: register('positron-new-folder-from-git', 0xf246), - positronX: register('positron-x', 0xf247), - positronAvailable1: register('positron-available-1', 0xf248), - positronVariables: register('positron-variables', 0xf249), - positronImportData: register('positron-import-data', 0xf24a), - positronList: register('positron-list', 0xf24b), - positronTable: register('positron-table', 0xf24c), - positronTest: register('positron-test', 0xf24d), - positronConsoleView: register('positron-console-view', 0xf24e), - positronWorkspace: register('positron-workspace', 0xf24f), - positronVariablesGrouping: register('positron-variables-grouping', 0xf250), - positronPlots: register('positron-plots', 0xf251), - positronVariablesSorting: register('positron-variables-sorting', 0xf253), - positronInterrupt: register('positron-interrupt', 0xf254), - positronClearSorting: register('positron-clear-sorting', 0xf255), - positronSearch: register('positron-search', 0xf256), - positronInterruptRuntime: register('positron-interrupt-runtime', 0xf257), - positronRestartRuntime: register('positron-restart-runtime', 0xf258), - positronPowerButton: register('positron-power-button', 0xf259), - positronMoreOptions: register('positron-more-options', 0xf25a), - positronTopActionBar: register('positron-top-action-bar', 0xf25b), - positronTriangleDown: register('positron-triangle-down', 0xf25c), - positronTriangleRight: register('positron-triangle-right', 0xf25d), - positronDataExplorerColumnsHidden: register('positron-data-explorer-columns-hidden', 0xf25e), - positronDataExplorerColumnsLeft: register('positron-data-explorer-columns-left', 0xf25f), - positronDataExplorerColumnsRight: register('positron-data-explorer-columns-right', 0xf260), - positronEllipsis: register('positron-ellipsis', 0xf261), - positronCheckMark: register('positron-check-mark', 0xf262), - positronVerticalEllipsis: register('positron-vertical-ellipsis', 0xf263), - positronDataTypeArray: register('positron-data-type-array', 0xf264), - positronDataTypeBoolean: register('positron-data-type-boolean', 0xf265), - positronDataTypeDateTime: register('positron-data-type-date-time', 0xf266), - positronDataTypeDate: register('positron-data-type-date', 0xf267), - positronDataTypeNumber: register('positron-data-type-number', 0xf268), - positronDataTypeString: register('positron-data-type-string', 0xf269), - positronDataTypeStruct: register('positron-data-type-struct', 0xf26a), - positronDataTypeTime: register('positron-data-type-time', 0xf26b), - positronDataTypeUnknown: register('positron-data-type-unknown', 0xf26c), - positronAddFilter: register('positron-add-filter', 0xf26d), - positronColumnFilter: register('positron-column-filter', 0xf26e), - positronRowFilter: register('positron-row-filter', 0xf26f), - positronHideFilters: register('positron-hide-filters', 0xf270), - positronShowFilters: register('positron-show-filters', 0xf271), - positronClearColumnFilters: register('positron-clear-column-filters', 0xf272), - positronClearRowFilters: register('positron-clear-row-filters', 0xf273), - positronPowerButtonThin: register('positron-power-button-thin', 0xf274), - positronRestartRuntimeThin: register('positron-restart-runtime-thin', 0xf275), - positronClearFilter: register('positron-clear-filter', 0xf276), - // --- End Positron --- - - - // 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/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts new file mode 100644 index 00000000000..d82cb713aa7 --- /dev/null +++ b/src/vs/base/common/codiconsLibrary.ts @@ -0,0 +1,652 @@ +/*--------------------------------------------------------------------------------------------- + * 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), + mapHorizontal: register('map-horizontal', 0xec05), + foldHorizontal: register('fold-horizontal', 0xec05), + mapFilled: register('map-filled', 0xec06), + mapHorizontalFilled: register('map-horizontal-filled', 0xec06), + foldHorizontalFilled: register('fold-horizontal-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), + mapVertical: register('map-vertical', 0xec30), + foldVertical: register('fold-vertical', 0xec30), + mapVerticalFilled: register('map-vertical-filled', 0xec31), + foldVerticalFilled: register('fold-vertical-filled', 0xec31), + goToSearch: register('go-to-search', 0xec32), + percentage: register('percentage', 0xec33), + sortPercentage: register('sort-percentage', 0xec33), + positronNew: register('positron-new', 0xf230), + positronOpen: register('positron-open', 0xf231), + positronSave: register('positron-save', 0xf232), + positronSaveAll: register('positron-save-all', 0xf233), + positronPrint: register('positron-print', 0xf234), + positronDropDownArrow: register('positron-drop-down-arrow', 0xf235), + positronLogo: register('positron-logo', 0xf236), + positronPositLogo: register('positron-posit-logo', 0xf237), + positronSeparator: register('positron-separator', 0xf238), + positronVariablesView: register('positron-variables-view', 0xf239), + positronHelpView: register('positron-help-view', 0xf23a), + positronOutlineView: register('positron-outline-view', 0xf23b), + positronPlotView: register('positron-plot-view', 0xf23c), + positronPreviewView: register('positron-preview-view', 0xf23d), + positronLeftArrow: register('positron-left-arrow', 0xf23e), + positronRightArrow: register('positron-right-arrow', 0xf23f), + positronHome: register('positron-home', 0xf240), + positronRefresh: register('positron-refresh', 0xf241), + positronOpenInNewWindow: register('positron-open-in-new-window', 0xf242), + positronSearchIcon: register('positron-search-icon', 0xf243), + positronSearchCancel: register('positron-search-cancel', 0xf244), + positronShowTraceback: register('positron-show-traceback', 0xf245), + positronNewFolderFromGit: register('positron-new-folder-from-git', 0xf246), + positronX: register('positron-x', 0xf247), + positronAvailableOne: register('positron-available-one', 0xf248), + positronVariables: register('positron-variables', 0xf249), + positronImportData: register('positron-import-data', 0xf24a), + positronList: register('positron-list', 0xf24b), + positronTable: register('positron-table', 0xf24c), + positronTest: register('positron-test', 0xf24d), + positronConsoleView: register('positron-console-view', 0xf24e), + positronWorkspace: register('positron-workspace', 0xf24f), + positronVariablesGrouping: register('positron-variables-grouping', 0xf250), + positronPlots: register('positron-plots', 0xf251), + positronPlotsView: register('positron-plots-view', 0xf252), + positronVariablesSorting: register('positron-variables-sorting', 0xf253), + positronInterrupt: register('positron-interrupt', 0xf254), + positronClearSorting: register('positron-clear-sorting', 0xf255), + positronSearch: register('positron-search', 0xf256), + positronInterruptRuntime: register('positron-interrupt-runtime', 0xf257), + positronRestartRuntime: register('positron-restart-runtime', 0xf258), + positronPowerButton: register('positron-power-button', 0xf259), + positronMoreOptions: register('positron-more-options', 0xf25a), + positronTopActionBar: register('positron-top-action-bar', 0xf25b), + positronTriangleDown: register('positron-triangle-down', 0xf25c), + positronTriangleRight: register('positron-triangle-right', 0xf25d), + positronDataToolColumnsHidden: register('positron-data-tool-columns-hidden', 0xf25e), + positronDataToolColumnsLeft: register('positron-data-tool-columns-left', 0xf25f), + positronDataToolColumnsRight: register('positron-data-tool-columns-right', 0xf260), + positronEllipsis: register('positron-ellipsis', 0xf261), + positronCheckMark: register('positron-check-mark', 0xf262), + positronVerticalEllipsis: register('positron-vertical-ellipsis', 0xf263), + positronDataTypeArray: register('positron-data-type-array', 0xf264), + positronDataTypeBoolean: register('positron-data-type-boolean', 0xf265), + positronDataTypeDateTime: register('positron-data-type-date-time', 0xf266), + positronDataTypeDate: register('positron-data-type-date', 0xf267), + positronDataTypeNumber: register('positron-data-type-number', 0xf268), + positronDataTypeString: register('positron-data-type-string', 0xf269), + positronDataTypeStruct: register('positron-data-type-struct', 0xf26a), + positronDataTypeTime: register('positron-data-type-time', 0xf26b), + positronDataTypeUnknown: register('positron-data-type-unknown', 0xf26c), + positronAddFilter: register('positron-add-filter', 0xf26d), + positronColumnFilter: register('positron-column-filter', 0xf26e), + positronRowFilter: register('positron-row-filter', 0xf26f), + positronHideFilters: register('positron-hide-filters', 0xf270), + positronShowFilters: register('positron-show-filters', 0xf271), + positronClearColumnFilters: register('positron-clear-column-filters', 0xf272), + positronClearRowFilters: register('positron-clear-row-filters', 0xf273), + positronPowerButtonThin: register('positron-power-button-thin', 0xf274), + positronRestartRuntimeThin: register('positron-restart-runtime-thin', 0xf275), + positronClearFilter: register('positron-clear-filter', 0xf276), +} as const; diff --git a/src/vs/base/common/codiconsUtil.ts b/src/vs/base/common/codiconsUtil.ts new file mode 100644 index 00000000000..ce7f9b2dafb --- /dev/null +++ b/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/src/vs/base/common/dataTransfer.ts b/src/vs/base/common/dataTransfer.ts index bed42389897..9c9ac45640b 100644 --- a/src/vs/base/common/dataTransfer.ts +++ b/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/src/vs/base/common/hierarchicalKind.ts b/src/vs/base/common/hierarchicalKind.ts new file mode 100644 index 00000000000..a2edd614375 --- /dev/null +++ b/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/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index 81262c2f46a..4216b0e5c0d 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/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/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index e338e5b1c93..67189f5e155 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -7,6 +7,7 @@ import * as errors from 'vs/base/common/errors'; import * as platform from 'vs/base/common/platform'; import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; +import * as paths from 'vs/base/common/path'; export namespace Schemas { @@ -126,11 +127,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 { @@ -154,7 +150,7 @@ class RemoteAuthoritiesImpl { private readonly _connectionTokens: { [authority: string]: string | undefined } = Object.create(null); private _preferredWebSchema: 'http' | 'https' = 'http'; private _delegate: ((uri: URI) => URI) | null = null; - private _remoteResourcesPath: string = `/${Schemas.vscodeRemoteResource}`; + private _serverRootPath: string = '/'; setPreferredWebSchema(schema: 'http' | 'https') { this._preferredWebSchema = schema; @@ -164,8 +160,16 @@ class RemoteAuthoritiesImpl { this._delegate = delegate; } - setServerRootPath(serverRootPath: string): void { - this._remoteResourcesPath = `${serverRootPath}/${Schemas.vscodeRemoteResource}`; + setServerRootPath(product: { quality?: string; commit?: string }, serverBasePath: string | undefined): void { + this._serverRootPath = getServerRootPath(product, serverBasePath); + } + + getServerRootPath(): string { + return this._serverRootPath; + } + + private get _remoteResourcesPath(): string { + return paths.posix.join(this._serverRootPath, Schemas.vscodeRemoteResource); } set(authority: string, host: string, port: number): void { @@ -216,6 +220,10 @@ class RemoteAuthoritiesImpl { export const RemoteAuthorities = new RemoteAuthoritiesImpl(); +export function getServerRootPath(product: { quality?: string; commit?: string }, basePath: string | undefined): string { + return paths.posix.join(basePath ?? '/', `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`); +} + /** * A string pointing to a path inside the app. It should not begin with ./ or ../ */ diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 74b03df1438..4e738e63344 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/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/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 5831de89add..0ac31f7f881 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/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/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 3893fbc6fcd..2251c7db5ba 100644 --- a/src/vs/base/common/platform.ts +++ b/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/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 02d201239ef..d0cab97ed68 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -205,6 +205,7 @@ export interface IProductConfiguration { readonly commonlyUsedSettings?: string[]; readonly aiGeneratedWorkspaceTrust?: IAiGeneratedWorkspaceTrust; readonly gitHubEntitlement?: IGitHubEntitlement; + readonly chatWelcomeView?: IChatWelcomeView; } export interface ITunnelApplicationConfig { @@ -318,3 +319,9 @@ export interface IGitHubEntitlement { confirmationMessage: string; confirmationAction: string; } + +export interface IChatWelcomeView { + welcomeViewId: string; + welcomeViewTitle: string; + welcomeViewContent: string; +} diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 6ec11f03919..050de0ca181 100644 --- a/src/vs/base/common/strings.ts +++ b/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/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts index ee8f3f4eb31..a7ec9cf6d36 100644 --- a/src/vs/base/node/extpath.ts +++ b/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/src/vs/base/node/osDisplayProtocolInfo.ts b/src/vs/base/node/osDisplayProtocolInfo.ts new file mode 100644 index 00000000000..c028dc88536 --- /dev/null +++ b/src/vs/base/node/osDisplayProtocolInfo.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { constants as FSConstants } from 'fs'; +import { access } from 'fs/promises'; +import { join } from 'vs/base/common/path'; +import { env } from 'vs/base/common/process'; + +const XDG_SESSION_TYPE = 'XDG_SESSION_TYPE'; +const WAYLAND_DISPLAY = 'WAYLAND_DISPLAY'; +const XDG_RUNTIME_DIR = 'XDG_RUNTIME_DIR'; + +const enum DisplayProtocolType { + Wayland = 'wayland', + XWayland = 'xwayland', + X11 = 'x11', + Unknown = 'unknown' +} + +export async function getDisplayProtocol(errorLogger: (error: any) => void): Promise { + const xdgSessionType = env[XDG_SESSION_TYPE]; + + if (xdgSessionType) { + // If XDG_SESSION_TYPE is set, return its value if it's either 'wayland' or 'x11'. + // We assume that any value other than 'wayland' or 'x11' is an error or unexpected, + // hence 'unknown' is returned. + return xdgSessionType === DisplayProtocolType.Wayland || xdgSessionType === DisplayProtocolType.X11 ? xdgSessionType : DisplayProtocolType.Unknown; + } else { + const waylandDisplay = env[WAYLAND_DISPLAY]; + + if (!waylandDisplay) { + // If WAYLAND_DISPLAY is empty, then the session is x11. + return DisplayProtocolType.X11; + } else { + const xdgRuntimeDir = env[XDG_RUNTIME_DIR]; + + if (!xdgRuntimeDir) { + // If XDG_RUNTIME_DIR is empty, then the session can only be guessed. + return DisplayProtocolType.Unknown; + } else { + // Check for the presence of the file $XDG_RUNTIME_DIR/wayland-0. + const waylandServerPipe = join(xdgRuntimeDir, 'wayland-0'); + + try { + await access(waylandServerPipe, FSConstants.R_OK); + + // If the file exists, then the session is wayland. + return DisplayProtocolType.Wayland; + } catch (err) { + // If the file does not exist or an error occurs, we guess 'unknown' + // since WAYLAND_DISPLAY was set but no wayland-0 pipe could be confirmed. + errorLogger(err); + return DisplayProtocolType.Unknown; + } + } + } + } +} + + +export function getCodeDisplayProtocol(displayProtocol: DisplayProtocolType, ozonePlatform: string | undefined): DisplayProtocolType { + if (!ozonePlatform) { + return displayProtocol === DisplayProtocolType.Wayland ? DisplayProtocolType.XWayland : DisplayProtocolType.X11; + } else { + switch (ozonePlatform) { + case 'auto': + return displayProtocol; + case 'x11': + return displayProtocol === DisplayProtocolType.Wayland ? DisplayProtocolType.XWayland : DisplayProtocolType.X11; + case 'wayland': + return DisplayProtocolType.Wayland; + default: + return DisplayProtocolType.Unknown; + } + } +} diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 3cb7bc9795b..c0d9b4b8ecb 100644 --- a/src/vs/base/node/zip.ts +++ b/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/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index f943347519e..6530fac0d7b 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/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/src/vs/base/parts/ipc/node/ipc.cp.ts b/src/vs/base/parts/ipc/node/ipc.cp.ts index 4fcad2758da..d51d77e3c81 100644 --- a/src/vs/base/parts/ipc/node/ipc.cp.ts +++ b/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/src/vs/base/test/browser/highlightedLabel.test.ts b/src/vs/base/test/browser/highlightedLabel.test.ts index 4f5eb5ca015..fe2ceb43d61 100644 --- a/src/vs/base/test/browser/highlightedLabel.test.ts +++ b/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/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts index 14de7bb4599..2ea6a9c1df9 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts +++ b/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/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index 766380ac8ec..73c04ad7239 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/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/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 7a039050402..578b00a796e 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -120,6 +120,7 @@ import { NODE_REMOTE_RESOURCE_CHANNEL_NAME, NODE_REMOTE_RESOURCE_IPC_METHOD_NAME import { Lazy } from 'vs/base/common/lazy'; import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { AuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService'; +import { normalizeNFC } from 'vs/base/common/normalization'; /** * The main VS Code application. There will only ever be one instance, @@ -193,7 +194,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}://`)) { @@ -435,6 +436,8 @@ export class CodeApplication extends Disposable { let macOpenFileURIs: IWindowOpenable[] = []; let runningTimeout: NodeJS.Timeout | undefined = undefined; app.on('open-file', (event, path) => { + path = normalizeNFC(path); // macOS only: normalize paths to NFC form + this.logService.trace('app#open-file: ', path); event.preventDefault(); @@ -1336,7 +1339,11 @@ export class CodeApplication extends Disposable { return windowsMainService.open({ context: OpenContext.DOCK, cli: args, - urisToOpen: macOpenFiles.map(path => (hasWorkspaceFileExtension(path) ? { workspaceUri: URI.file(path) } : { fileUri: URI.file(path) })), + urisToOpen: macOpenFiles.map(path => { + path = normalizeNFC(path); // macOS only: normalize paths to NFC form + + return (hasWorkspaceFileExtension(path) ? { workspaceUri: URI.file(path) } : { fileUri: URI.file(path) }); + }), noRecentEntry, waitMarkerFileURI, initialStartup: true, diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index c5466e20b45..8548fa7ce4d 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -71,6 +71,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { SaveStrategy, StateService } from 'vs/platform/state/node/stateService'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; /** * The main VS Code entry point. @@ -249,8 +250,8 @@ class CodeMain { // Environment service (paths) Promise.all([ - environmentMainService.extensionsPath, - environmentMainService.codeCachePath, + this.allowWindowsUNCPath(environmentMainService.extensionsPath), // enable extension paths on UNC drives... + environmentMainService.codeCachePath, // ...other user-data-derived paths should already be enlisted from `main.js` environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, userDataProfilesMainService.defaultProfile.globalStorageHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, @@ -269,6 +270,17 @@ class CodeMain { userDataProfilesMainService.init(); } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means diff --git a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts b/src/vs/code/electron-sandbox/issue/issueReporterModel.ts index 74f993903e1..1541f98c812 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts +++ b/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/src/vs/code/electron-sandbox/issue/issueReporterPage.ts b/src/vs/code/electron-sandbox/issue/issueReporterPage.ts index 4fbd26e606b..e208006b862 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterPage.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterPage.ts @@ -83,6 +83,7 @@ export default (): string => ` +
diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index fb0d8026e4d..a5e55b380ce 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/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(); } @@ -158,6 +166,7 @@ export class IssueReporter extends Disposable { } } + // TODO @justschen: After migration to Aux Window, switch to dedicated css. private applyStyles(styles: IssueReporterStyles) { const styleTag = document.createElement('style'); const content: string[] = []; @@ -503,6 +512,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() { @@ -763,13 +797,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; } } @@ -906,13 +944,24 @@ export class IssueReporter extends Disposable { private validateInput(inputId: string): boolean { const inputElement = (this.getElementById(inputId)); const inputValidationMessage = this.getElementById(`${inputId}-empty-error`); + const descriptionShortMessage = this.getElementById(`description-short-error`); if (!inputElement.value) { inputElement.classList.add('invalid-input'); inputValidationMessage?.classList.remove('hidden'); + descriptionShortMessage?.classList.add('hidden'); return false; - } else { + } else if (inputId === 'description' && inputElement.value.length < 10) { + inputElement.classList.add('invalid-input'); + descriptionShortMessage?.classList.remove('hidden'); + inputValidationMessage?.classList.add('hidden'); + return false; + } + else { inputElement.classList.remove('invalid-input'); inputValidationMessage?.classList.add('hidden'); + if (inputId === 'description') { + descriptionShortMessage?.classList.add('hidden'); + } return true; } } diff --git a/src/vs/code/electron-sandbox/issue/media/issueReporter.css b/src/vs/code/electron-sandbox/issue/media/issueReporter.css index 8c452086409..152d6c38bc8 100644 --- a/src/vs/code/electron-sandbox/issue/media/issueReporter.css +++ b/src/vs/code/electron-sandbox/issue/media/issueReporter.css @@ -72,7 +72,7 @@ textarea { width: auto; padding: 4px 10px; align-self: flex-end; - margin-bottom: 10px; + margin-bottom: 1em; font-size: 13px; } @@ -157,7 +157,8 @@ body { padding-bottom: 2em; display: flex; flex-direction: column; - height: 100%; + min-height: 100%; + overflow: visible; } .description-section { @@ -213,6 +214,10 @@ select, input, textarea { border-top: 0px !important; } +#issue-reporter .system-info { + margin-bottom: 10px; +} + input[type="checkbox"] { width: auto; @@ -364,6 +369,7 @@ a { } .issues-container > .issue > .issue-state { + display: flex; width: 77px; padding: 3px 6px; margin-right: 5px; @@ -373,8 +379,13 @@ a { } .issues-container > .issue .label { + padding-top: 2px; margin-left: 5px; width: 44px; text-overflow: ellipsis; overflow: hidden; } + +.issues-container > .issue .issue-icon{ + padding-top: 2px; +} diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index b91367f1fc2..aea83578ee0 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -63,6 +63,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { localize } from 'vs/nls'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; class CliMain extends Disposable { @@ -121,8 +122,8 @@ class CliMain extends Disposable { // Init folders await Promise.all([ - environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath, - environmentService.extensionsPath + this.allowWindowsUNCPath(environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath), + this.allowWindowsUNCPath(environmentService.extensionsPath) ].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Logger @@ -233,6 +234,17 @@ class CliMain extends Disposable { return [new InstantiationService(services), appenders]; } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private registerErrorHandler(logService: ILogService): void { // Install handler for unexpected errors diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 3c0de38db43..9a59ccce5c3 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -118,6 +118,7 @@ import { NativeEnvironmentService } from 'vs/platform/environment/node/environme import { SharedProcessRawConnection, SharedProcessLifecycle } from 'vs/platform/sharedProcess/common/sharedProcess'; import { getOSReleaseInfo } from 'vs/base/node/osReleaseInfo'; import { getDesktopEnvironment } from 'vs/base/common/desktopEnvironmentInfo'; +import { getCodeDisplayProtocol, getDisplayProtocol } from 'vs/base/node/osDisplayProtocolInfo'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -465,14 +466,20 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { private async reportClientOSInfo(telemetryService: ITelemetryService, logService: ILogService): Promise { if (isLinux) { - const releaseInfo = await getOSReleaseInfo(logService.error.bind(logService)); + const [releaseInfo, displayProtocol] = await Promise.all([ + getOSReleaseInfo(logService.error.bind(logService)), + getDisplayProtocol(logService.error.bind(logService)) + ]); const desktopEnvironment = getDesktopEnvironment(); + const codeSessionType = getCodeDisplayProtocol(displayProtocol, this.configuration.args['ozone-platform']); if (releaseInfo) { type ClientPlatformInfoClassification = { platformId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system without any version information.' }; platformVersionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system version excluding any name information or release code.' }; platformIdLike: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system the current OS derivate is closely related to.' }; desktopEnvironment: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the desktop environment the user is using.' }; + displayProtocol: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the users display protocol type.' }; + codeDisplayProtocol: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the vscode display protocol type.' }; owner: 'benibenj'; comment: 'Provides insight into the distro and desktop environment information on Linux.'; }; @@ -481,12 +488,16 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { platformVersionId: string | undefined; platformIdLike: string | undefined; desktopEnvironment: string | undefined; + displayProtocol: string | undefined; + codeDisplayProtocol: string | undefined; }; telemetryService.publicLog2('clientPlatformInfo', { platformId: releaseInfo.id, platformVersionId: releaseInfo.version_id, platformIdLike: releaseInfo.id_like, - desktopEnvironment: desktopEnvironment + desktopEnvironment: desktopEnvironment, + displayProtocol: displayProtocol, + codeDisplayProtocol: codeSessionType }); } } @@ -519,15 +530,24 @@ export async function main(configuration: ISharedProcessConfiguration): Promise< // create shared process and signal back to main that we are // ready to accept message ports as client connections - const sharedProcess = new SharedProcessMain(configuration); - process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); + try { + const sharedProcess = new SharedProcessMain(configuration); + process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); - // await initialization and signal this back to electron-main - await sharedProcess.init(); + // await initialization and signal this back to electron-main + await sharedProcess.init(); - process.parentPort.postMessage(SharedProcessLifecycle.initDone); + process.parentPort.postMessage(SharedProcessLifecycle.initDone); + } catch (error) { + process.parentPort.postMessage({ error: error.toString() }); + } } +const handle = setTimeout(() => { + process.parentPort.postMessage({ warning: '[SharedProcess] did not receive configuration within 30s...' }); +}, 30000); + process.parentPort.once('message', (e: Electron.MessageEvent) => { + clearTimeout(handle); main(e.data as ISharedProcessConfiguration); }); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 8598d9140b9..4a1addb3fc0 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -21,6 +21,7 @@ import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/cursor/curs import { PositionAffinity } from 'vs/editor/common/model'; import { InjectedText } from 'vs/editor/common/modelLineProjectionData'; import { Mutable } from 'vs/base/common/types'; +import { Lazy } from 'vs/base/common/lazy'; const enum HitTestResultType { Unknown, @@ -36,6 +37,9 @@ class UnknownHitTestResult { class ContentHitTestResult { readonly type = HitTestResultType.Content; + + get hitTarget(): HTMLElement { return this.spanNode; } + constructor( readonly position: Position, readonly spanNode: HTMLElement, @@ -404,26 +408,53 @@ abstract class BareHitTestRequest { class HitTestRequest extends BareHitTestRequest { private readonly _ctx: HitTestContext; - public readonly target: HTMLElement | null; - public readonly targetPath: Uint8Array; + private readonly _eventTarget: HTMLElement | null; + public readonly hitTestResult = new Lazy(() => MouseTargetFactory.doHitTest(this._ctx, this)); + private _useHitTestTarget: boolean; + private _targetPathCacheElement: HTMLElement | null = null; + private _targetPathCacheValue: Uint8Array = new Uint8Array(0); + + public get target(): HTMLElement | null { + if (this._useHitTestTarget) { + return this.hitTestResult.value.hitTarget; + } + return this._eventTarget; + } - constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: HTMLElement | null) { + public get targetPath(): Uint8Array { + if (this._targetPathCacheElement !== this.target) { + this._targetPathCacheElement = this.target; + this._targetPathCacheValue = PartFingerprints.collect(this.target, this._ctx.viewDomNode); + } + return this._targetPathCacheValue; + } + + constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, eventTarget: HTMLElement | null) { super(ctx, editorPos, pos, relativePos); this._ctx = ctx; + this._eventTarget = eventTarget; - if (target) { - this.target = target; - this.targetPath = PartFingerprints.collect(target, ctx.viewDomNode); - } else { - this.target = null; - this.targetPath = new Uint8Array(0); - } + // If no event target is passed in, we will use the hit test target + const hasEventTarget = Boolean(this._eventTarget); + this._useHitTestTarget = !hasEventTarget; } public override toString(): string { return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), relativePos(${this.relativePos.x},${this.relativePos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? (this.target).outerHTML : null}`; } + public get wouldBenefitFromHitTestTargetSwitch(): boolean { + return ( + !this._useHitTestTarget + && this.hitTestResult.value.hitTarget !== null + && this.target !== this.hitTestResult.value.hitTarget + ); + } + + public switchToHitTestTarget(): void { + this._useHitTestTarget = true; + } + private _getMouseColumn(position: Position | null = null): number { if (position && position.column < this._ctx.viewModel.getLineMaxColumn(position.lineNumber)) { // Most likely, the line contains foreign decorations... @@ -459,10 +490,6 @@ class HitTestRequest extends BareHitTestRequest { public fulfillOverlayWidget(detail: string): IMouseTargetOverlayWidget { return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail); } - - public withTarget(target: HTMLElement | null): HitTestRequest { - return new HitTestRequest(this._ctx, this.editorPos, this.pos, this.relativePos, target); - } } interface ResolvedHitTestRequest extends HitTestRequest { @@ -509,7 +536,7 @@ export class MouseTargetFactory { const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData); const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target); try { - const r = MouseTargetFactory._createMouseTarget(ctx, request, false); + const r = MouseTargetFactory._createMouseTarget(ctx, request); if (r.type === MouseTargetType.CONTENT_TEXT) { // Snap to the nearest soft tab boundary if atomic soft tabs are enabled. @@ -528,24 +555,13 @@ export class MouseTargetFactory { } } - private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): IMouseTarget { + private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest): IMouseTarget { // console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`); - // First ensure the request has a target if (request.target === null) { - if (domHitTestExecuted) { - // Still no target... and we have already executed hit test... - return request.fulfillUnknown(); - } - - const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); - - if (hitTestResult.type === HitTestResultType.Content) { - return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText); - } - - return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); + // No target + return request.fulfillUnknown(); } // we know for a fact that request.target is not null @@ -566,7 +582,7 @@ export class MouseTargetFactory { result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest); - result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest, domHitTestExecuted); + result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest); return (result || request.fulfillUnknown()); @@ -704,7 +720,7 @@ export class MouseTargetFactory { return null; } - private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest, domHitTestExecuted: boolean): IMouseTarget | null { + private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { if (!ElementPath.isChildOfViewLines(request.targetPath)) { return null; } @@ -721,36 +737,41 @@ export class MouseTargetFactory { return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES); } - if (domHitTestExecuted) { - // Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines) - // See https://github.com/microsoft/vscode/issues/46942 - if (ElementPath.isStrictChildOfViewLines(request.targetPath)) { - const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); - if (ctx.viewModel.getLineLength(lineNumber) === 0) { - const lineWidth = ctx.getLineWidth(lineNumber); - const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - return request.fulfillContentEmpty(new Position(lineNumber, 1), detail); - } - + // Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines) + // See https://github.com/microsoft/vscode/issues/46942 + if (ElementPath.isStrictChildOfViewLines(request.targetPath)) { + const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); + if (ctx.viewModel.getLineLength(lineNumber) === 0) { const lineWidth = ctx.getLineWidth(lineNumber); - if (request.mouseContentHorizontalOffset >= lineWidth) { - const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber)); - return request.fulfillContentEmpty(pos, detail); - } + const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); + return request.fulfillContentEmpty(new Position(lineNumber, 1), detail); } - // We have already executed hit test... - return request.fulfillUnknown(); + const lineWidth = ctx.getLineWidth(lineNumber); + if (request.mouseContentHorizontalOffset >= lineWidth) { + // TODO: This is wrong for RTL + const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); + const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber)); + return request.fulfillContentEmpty(pos, detail); + } } - const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); + // Do the hit test (if not already done) + const hitTestResult = request.hitTestResult.value; if (hitTestResult.type === HitTestResultType.Content) { return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText); } - return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); + // We didn't hit content... + if (request.wouldBenefitFromHitTestTargetSwitch) { + // We actually hit something different... Give it one last change by trying again with this new target + request.switchToHitTestTarget(); + return this._createMouseTarget(ctx, request); + } + + // We have tried everything... + return request.fulfillUnknown(); } private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { @@ -1019,7 +1040,7 @@ export class MouseTargetFactory { return position; } - private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult { + public static doHitTest(ctx: HitTestContext, request: BareHitTestRequest): HitTestResult { let result: HitTestResult = new UnknownHitTestResult(); if (typeof (ctx.viewDomNode.ownerDocument).caretRangeFromPoint === 'function') { diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index f1661da1fc4..c8e5b7e50a4 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/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/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index 927b24981dc..ca0a4bb8a1f 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -397,7 +397,7 @@ export namespace CoreNavigationCommands { ] ); if (cursorStateChanged && args.revealType !== NavigationCommandRevealType.None) { - viewModel.revealPrimaryCursor(args.source, true, true); + viewModel.revealAllCursors(args.source, true, true); } } } @@ -609,7 +609,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, CursorMoveImpl._move(viewModel, viewModel.getCursorStates(), args) ); - viewModel.revealPrimaryCursor(source, true); + viewModel.revealAllCursors(source, true); } private static _move(viewModel: IViewModel, cursors: CursorState[], args: CursorMove_.ParsedArguments): PartialCursorState[] | null { @@ -678,7 +678,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, CursorMoveCommands.simpleMove(viewModel, viewModel.getCursorStates(), args.direction, args.select, args.value, args.unit) ); - viewModel.revealPrimaryCursor(dynamicArgs.source, true); + viewModel.revealAllCursors(dynamicArgs.source, true); } } @@ -993,7 +993,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, CursorMoveCommands.moveToBeginningOfLine(viewModel, viewModel.getCursorStates(), this._inSelectionMode) ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } } @@ -1037,7 +1037,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, this._exec(viewModel.getCursorStates()) ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } private _exec(cursors: CursorState[]): PartialCursorState[] { @@ -1095,7 +1095,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, CursorMoveCommands.moveToEndOfLine(viewModel, viewModel.getCursorStates(), this._inSelectionMode, args.sticky || false) ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } } @@ -1173,7 +1173,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, this._exec(viewModel, viewModel.getCursorStates()) ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } private _exec(viewModel: IViewModel, cursors: CursorState[]): PartialCursorState[] { @@ -1228,7 +1228,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, CursorMoveCommands.moveToBeginningOfBuffer(viewModel, viewModel.getCursorStates(), this._inSelectionMode) ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } } @@ -1272,7 +1272,7 @@ export namespace CoreNavigationCommands { CursorChangeReason.Explicit, CursorMoveCommands.moveToEndOfBuffer(viewModel, viewModel.getCursorStates(), this._inSelectionMode) ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } } @@ -1644,7 +1644,7 @@ export namespace CoreNavigationCommands { ] ); if (args.revealType !== NavigationCommandRevealType.None) { - viewModel.revealPrimaryCursor(args.source, true, true); + viewModel.revealAllCursors(args.source, true, true); } } } @@ -1710,7 +1710,7 @@ export namespace CoreNavigationCommands { ] ); if (args.revealType !== NavigationCommandRevealType.None) { - viewModel.revealPrimaryCursor(args.source, false, true); + viewModel.revealAllCursors(args.source, false, true); } } } @@ -1789,7 +1789,7 @@ export namespace CoreNavigationCommands { CursorMoveCommands.cancelSelection(viewModel, viewModel.getPrimaryCursorState()) ] ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } }); @@ -1816,7 +1816,7 @@ export namespace CoreNavigationCommands { viewModel.getPrimaryCursorState() ] ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); status(nls.localize('removedCursor', "Removed secondary cursors")); } }); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index ecedcdb42a9..7678c6e3b88 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/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/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index f707c6fa25e..aa1e41c8985 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/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/src/vs/editor/browser/widget/hoverWidget/hover.css b/src/vs/editor/browser/services/hoverService/hover.css similarity index 100% rename from src/vs/editor/browser/widget/hoverWidget/hover.css rename to src/vs/editor/browser/services/hoverService/hover.css diff --git a/src/vs/editor/browser/services/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts similarity index 90% rename from src/vs/editor/browser/services/hoverService.ts rename to src/vs/editor/browser/services/hoverService/hoverService.ts index 9dea5dab812..38357608b86 100644 --- a/src/vs/editor/browser/services/hoverService.ts +++ b/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/widget/hoverWidget/hoverWidget'; +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/src/vs/editor/browser/widget/hoverWidget/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts similarity index 96% rename from src/vs/editor/browser/widget/hoverWidget/hoverWidget.ts rename to src/vs/editor/browser/services/hoverService/hoverWidget.ts index d6f6249675c..24ae9cf483e 100644 --- a/src/vs/editor/browser/widget/hoverWidget/hoverWidget.ts +++ b/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 = { @@ -331,10 +331,7 @@ export class HoverWidget extends Widget implements IHoverWidget { }; const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e)); - const top = Math.min(...targetBounds.map(e => e.top)); - const right = Math.max(...targetBounds.map(e => e.right)); - const bottom = Math.max(...targetBounds.map(e => e.bottom)); - const left = Math.min(...targetBounds.map(e => e.left)); + const { top, right, bottom, left } = targetBounds[0]; const width = right - left; const height = bottom - top; @@ -472,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) { @@ -487,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 @@ -504,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 @@ -516,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; } } @@ -529,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; } } @@ -540,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/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index ba0b5008760..a803234c4b4 100644 --- a/src/vs/editor/browser/view.ts +++ b/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/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index c15239ec8b1..bbbb0dd9d73 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/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/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 83a3cc05d6f..1041fd58a58 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/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/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css index 2a0e39dffa7..403e255fac8 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css +++ b/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/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 64649e0b835..b35970ee373 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/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/src/vs/editor/browser/viewParts/decorations/decorations.css b/src/vs/editor/browser/viewParts/decorations/decorations.css index 37c39f620e8..4c755e2dbf8 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.css +++ b/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/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index fe495466b1d..a3baa510464 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/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/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css index ed132669757..6aacf7c2126 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css +++ b/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/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index a93cf75a530..50b0b2b8661 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/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/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css index 774ffef273d..2961137b032 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css +++ b/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/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index 03336d34278..dcebb27994e 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/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/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index e4174a2f286..9a5d2f556bf 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/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/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index fe686d3e441..899c96b8cfc 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -63,6 +63,13 @@ 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 { + top: 0; + bottom: 0; + position: absolute; +} + .monaco-editor .mtkw { color: var(--vscode-editorWhitespace-foreground) !important; } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 789b68836b9..427df7cd3d8 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/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/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 1338656acab..0ca31065a45 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { Position } from 'vs/editor/common/core/position'; import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { TokenizationRegistry } from 'vs/editor/common/languages'; -import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground } from 'vs/editor/common/core/editorColorRegistry'; +import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground, editorMultiCursorSecondaryForeground, editorMultiCursorPrimaryForeground } from 'vs/editor/common/core/editorColorRegistry'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { EditorTheme } from 'vs/editor/common/editorTheme'; @@ -29,7 +29,9 @@ class Settings { public readonly borderColor: string | null; public readonly hideCursor: boolean; - public readonly cursorColor: string | null; + public readonly cursorColorSingle: string | null; + public readonly cursorColorPrimary: string | null; + public readonly cursorColorSecondary: string | null; public readonly themeType: 'light' | 'dark' | 'hcLight' | 'hcDark'; public readonly backgroundColor: Color | null; @@ -55,8 +57,12 @@ class Settings { this.borderColor = borderColor ? borderColor.toString() : null; this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler); - const cursorColor = theme.getColor(editorCursorForeground); - this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; + const cursorColorSingle = theme.getColor(editorCursorForeground); + this.cursorColorSingle = cursorColorSingle ? cursorColorSingle.transparent(0.7).toString() : null; + const cursorColorPrimary = theme.getColor(editorMultiCursorPrimaryForeground); + this.cursorColorPrimary = cursorColorPrimary ? cursorColorPrimary.transparent(0.7).toString() : null; + const cursorColorSecondary = theme.getColor(editorMultiCursorSecondaryForeground); + this.cursorColorSecondary = cursorColorSecondary ? cursorColorSecondary.transparent(0.7).toString() : null; this.themeType = theme.type; @@ -189,7 +195,9 @@ class Settings { && this.renderBorder === other.renderBorder && this.borderColor === other.borderColor && this.hideCursor === other.hideCursor - && this.cursorColor === other.cursorColor + && this.cursorColorSingle === other.cursorColorSingle + && this.cursorColorPrimary === other.cursorColorPrimary + && this.cursorColorSecondary === other.cursorColorSecondary && this.themeType === other.themeType && Color.equals(this.backgroundColor, other.backgroundColor) && this.top === other.top @@ -213,6 +221,11 @@ const enum OverviewRulerLane { Full = 7 } +type Cursor = { + position: Position; + color: string | null; +}; + const enum ShouldRenderValue { NotNeeded = 0, Maybe = 1, @@ -226,10 +239,10 @@ export class DecorationsOverviewRuler extends ViewPart { private readonly _tokensColorTrackerListener: IDisposable; private readonly _domNode: FastDomNode; private _settings!: Settings; - private _cursorPositions: Position[]; + private _cursorPositions: Cursor[]; private _renderedDecorations: OverviewRulerDecorationsGroup[] = []; - private _renderedCursorPositions: Position[] = []; + private _renderedCursorPositions: Cursor[] = []; constructor(context: ViewContext) { super(context); @@ -249,7 +262,7 @@ export class DecorationsOverviewRuler extends ViewPart { } }); - this._cursorPositions = [new Position(1, 1)]; + this._cursorPositions = [{ position: new Position(1, 1), color: this._settings.cursorColorSingle }]; } public override dispose(): void { @@ -298,9 +311,13 @@ export class DecorationsOverviewRuler extends ViewPart { public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { this._cursorPositions = []; for (let i = 0, len = e.selections.length; i < len; i++) { - this._cursorPositions[i] = e.selections[i].getPosition(); + let color = this._settings.cursorColorSingle; + if (len > 1) { + color = i === 0 ? this._settings.cursorColorPrimary : this._settings.cursorColorSecondary; + } + this._cursorPositions.push({ position: e.selections[i].getPosition(), color }); } - this._cursorPositions.sort(Position.compare); + this._cursorPositions.sort((a, b) => Position.compare(a.position, b.position)); return this._markRenderingIsMaybeNeeded(); } public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { @@ -352,7 +369,7 @@ export class DecorationsOverviewRuler extends ViewPart { if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) { this._actualShouldRender = ShouldRenderValue.Needed; } - if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.lineNumber === b.lineNumber)) { + if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.position.lineNumber === b.position.lineNumber && a.color === b.color)) { this._actualShouldRender = ShouldRenderValue.Needed; } if (this._actualShouldRender === ShouldRenderValue.Maybe) { @@ -443,17 +460,21 @@ export class DecorationsOverviewRuler extends ViewPart { } // Draw cursors - if (!this._settings.hideCursor && this._settings.cursorColor) { + if (!this._settings.hideCursor) { const cursorHeight = (2 * this._settings.pixelRatio) | 0; const halfCursorHeight = (cursorHeight / 2) | 0; const cursorX = this._settings.x[OverviewRulerLane.Full]; const cursorW = this._settings.w[OverviewRulerLane.Full]; - canvasCtx.fillStyle = this._settings.cursorColor; let prevY1 = -100; let prevY2 = -100; + let prevColor: string | null = null; for (let i = 0, len = this._cursorPositions.length; i < len; i++) { - const cursor = this._cursorPositions[i]; + const color = this._cursorPositions[i].color; + if (!color) { + continue; + } + const cursor = this._cursorPositions[i].position; let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0; if (yCenter < halfCursorHeight) { @@ -464,9 +485,9 @@ export class DecorationsOverviewRuler extends ViewPart { const y1 = yCenter - halfCursorHeight; const y2 = y1 + cursorHeight; - if (y1 > prevY2 + 1) { + if (y1 > prevY2 + 1 || color !== prevColor) { // flush prev - if (i !== 0) { + if (i !== 0 && prevColor) { canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1); } prevY1 = y1; @@ -477,8 +498,12 @@ export class DecorationsOverviewRuler extends ViewPart { prevY2 = y2; } } + prevColor = color; + canvasCtx.fillStyle = color; + } + if (prevColor) { + canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1); } - canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1); } if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) { diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index efceef0e5c3..d53a5126e62 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/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/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index 36506e9fed0..4502698cfc5 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -35,6 +35,12 @@ class ViewCursorRenderData { ) { } } +export enum CursorPlurality { + Single, + MultiPrimary, + MultiSecondary, +} + export class ViewCursor { private readonly _context: ViewContext; private readonly _domNode: FastDomNode; @@ -47,11 +53,12 @@ export class ViewCursor { private _isVisible: boolean; private _position: Position; + private _pluralityClass: string; private _lastRenderedContent: string; private _renderData: ViewCursorRenderData | null; - constructor(context: ViewContext) { + constructor(context: ViewContext, plurality: CursorPlurality) { this._context = context; const options = this._context.configuration.options; const fontInfo = options.get(EditorOption.fontInfo); @@ -73,6 +80,8 @@ export class ViewCursor { this._domNode.setDisplay('none'); this._position = new Position(1, 1); + this._pluralityClass = ''; + this.setPlurality(plurality); this._lastRenderedContent = ''; this._renderData = null; @@ -86,6 +95,23 @@ export class ViewCursor { return this._position; } + public setPlurality(plurality: CursorPlurality) { + switch (plurality) { + default: + case CursorPlurality.Single: + this._pluralityClass = ''; + break; + + case CursorPlurality.MultiPrimary: + this._pluralityClass = 'cursor-primary'; + break; + + case CursorPlurality.MultiSecondary: + this._pluralityClass = 'cursor-secondary'; + break; + } + } + public show(): void { if (!this._isVisible) { this._domNode.setVisibility('inherit'); @@ -229,7 +255,7 @@ export class ViewCursor { this._domNode.domNode.textContent = this._lastRenderedContent; } - this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`); + this._domNode.setClassName(`cursor ${this._pluralityClass} ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`); this._domNode.setDisplay('block'); this._domNode.setTop(this._renderData.top); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index 1be969b7497..ceb27ce5ea3 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -7,10 +7,14 @@ import 'vs/css!./viewCursors'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; -import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; +import { IViewCursorRenderData, ViewCursor, CursorPlurality } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; -import { editorCursorBackground, editorCursorForeground } from 'vs/editor/common/core/editorColorRegistry'; +import { + editorCursorBackground, editorCursorForeground, + editorMultiCursorPrimaryForeground, editorMultiCursorPrimaryBackground, + editorMultiCursorSecondaryForeground, editorMultiCursorSecondaryBackground +} from 'vs/editor/common/core/editorColorRegistry'; 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'; @@ -57,7 +61,7 @@ export class ViewCursors extends ViewPart { this._isVisible = false; - this._primaryCursor = new ViewCursor(this._context); + this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single); this._secondaryCursors = []; this._renderData = []; @@ -88,6 +92,7 @@ export class ViewCursors extends ViewPart { } // --- begin event handlers + public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean { this._isComposingInput = true; this._updateBlinking(); @@ -120,6 +125,7 @@ export class ViewCursors extends ViewPart { this._secondaryCursors.length !== secondaryPositions.length || (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit) ); + this._primaryCursor.setPlurality(secondaryPositions.length ? CursorPlurality.MultiPrimary : CursorPlurality.Single); this._primaryCursor.onCursorPositionChanged(position, pauseAnimation); this._updateBlinking(); @@ -127,7 +133,7 @@ export class ViewCursors extends ViewPart { // Create new cursors const addCnt = secondaryPositions.length - this._secondaryCursors.length; for (let i = 0; i < addCnt; i++) { - const newCursor = new ViewCursor(this._context); + const newCursor = new ViewCursor(this._context, CursorPlurality.MultiSecondary); this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling); this._secondaryCursors.push(newCursor); } @@ -160,7 +166,6 @@ export class ViewCursors extends ViewPart { return true; } - public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { // true for inline decorations that can end up relayouting text return true; @@ -263,6 +268,7 @@ export class ViewCursors extends ViewPart { } } } + // --- end blinking logic private _updateDomClassName(): void { @@ -375,16 +381,29 @@ export class ViewCursors extends ViewPart { } registerThemingParticipant((theme, collector) => { - const caret = theme.getColor(editorCursorForeground); - if (caret) { - let caretBackground = theme.getColor(editorCursorBackground); - if (!caretBackground) { - caretBackground = caret.opposite(); - } - collector.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`); - if (isHighContrast(theme.type)) { - collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`); + type CursorTheme = { + foreground: string; + background: string; + class: string; + }; + + const cursorThemes: CursorTheme[] = [ + { class: '.cursor', foreground: editorCursorForeground, background: editorCursorBackground }, + { class: '.cursor-primary', foreground: editorMultiCursorPrimaryForeground, background: editorMultiCursorPrimaryBackground }, + { class: '.cursor-secondary', foreground: editorMultiCursorSecondaryForeground, background: editorMultiCursorSecondaryBackground }, + ]; + + for (const cursorTheme of cursorThemes) { + const caret = theme.getColor(cursorTheme.foreground); + if (caret) { + let caretBackground = theme.getColor(cursorTheme.background); + if (!caretBackground) { + caretBackground = caret.opposite(); + } + collector.addRule(`.monaco-editor .cursors-layer ${cursorTheme.class} { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`); + if (isHighContrast(theme.type)) { + collector.addRule(`.monaco-editor .cursors-layer.has-selection ${cursorTheme.class} { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`); + } } } - }); diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 489293a01b8..3bd29fc5e1e 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/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/src/vs/editor/browser/widget/codeEditorContributions.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorContributions.ts similarity index 100% rename from src/vs/editor/browser/widget/codeEditorContributions.ts rename to src/vs/editor/browser/widget/codeEditor/codeEditorContributions.ts diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts similarity index 99% rename from src/vs/editor/browser/widget/codeEditorWidget.ts rename to src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 5466f913d24..108aa52fa8a 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/editor/browser/services/markerDecorations'; - import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; @@ -14,7 +13,7 @@ import { Emitter, EmitterOptions, Event, EventDeliveryQueue, createEventDelivery import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import 'vs/css!./media/editor'; +import 'vs/css!./editor'; import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; import { EditorConfiguration, IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { TabFocus } from 'vs/editor/browser/config/tabFocus'; @@ -25,7 +24,7 @@ import { IContentWidgetData, IGlyphMarginWidgetData, IOverlayWidgetData, View } import { DOMLineBreaksComputerFactory } from 'vs/editor/browser/view/domLineBreaksComputer'; import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; -import { CodeEditorContributions } from 'vs/editor/browser/widget/codeEditorContributions'; +import { CodeEditorContributions } from 'vs/editor/browser/widget/codeEditor/codeEditorContributions'; import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { ConfigurationChangedEvent, EditorLayoutInfo, EditorOption, FindComputedEditorOptionValueById, IComputedEditorOptions, IEditorOptions, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; @@ -61,51 +60,6 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { editorErrorForeground, editorHintForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -let EDITOR_ID = 0; - -export interface ICodeEditorWidgetOptions { - /** - * Is this a simple widget (not a real code editor)? - * Defaults to false. - */ - isSimpleWidget?: boolean; - - /** - * Contributions to instantiate. - * When provided, only the contributions included will be instantiated. - * To include the defaults, those must be provided as well via [...EditorExtensionsRegistry.getEditorContributions()] - * Defaults to EditorExtensionsRegistry.getEditorContributions(). - */ - contributions?: IEditorContributionDescription[]; - - /** - * Telemetry data associated with this CodeEditorWidget. - * Defaults to null. - */ - telemetryData?: object; -} - -class ModelData { - constructor( - public readonly model: ITextModel, - public readonly viewModel: ViewModel, - public readonly view: View, - public readonly hasRealView: boolean, - public readonly listenersToRemove: IDisposable[], - public readonly attachedView: IAttachedView, - ) { - } - - public dispose(): void { - dispose(this.listenersToRemove); - this.model.onBeforeDetached(this.attachedView); - if (this.hasRealView) { - this.view.dispose(); - } - this.viewModel.dispose(); - } -} - export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeEditor { private static readonly dropIntoEditorDecorationOptions = ModelDecorationOptions.register({ @@ -464,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 { @@ -1088,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: @@ -1154,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; @@ -1164,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 }); @@ -1811,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) => { @@ -1932,6 +1887,51 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } } +let EDITOR_ID = 0; + +export interface ICodeEditorWidgetOptions { + /** + * Is this a simple widget (not a real code editor)? + * Defaults to false. + */ + isSimpleWidget?: boolean; + + /** + * Contributions to instantiate. + * When provided, only the contributions included will be instantiated. + * To include the defaults, those must be provided as well via [...EditorExtensionsRegistry.getEditorContributions()] + * Defaults to EditorExtensionsRegistry.getEditorContributions(). + */ + contributions?: IEditorContributionDescription[]; + + /** + * Telemetry data associated with this CodeEditorWidget. + * Defaults to null. + */ + telemetryData?: object; +} + +class ModelData { + constructor( + public readonly model: ITextModel, + public readonly viewModel: ViewModel, + public readonly view: View, + public readonly hasRealView: boolean, + public readonly listenersToRemove: IDisposable[], + public readonly attachedView: IAttachedView, + ) { + } + + public dispose(): void { + dispose(this.listenersToRemove); + this.model.onBeforeDetached(this.attachedView); + if (this.hasRealView) { + this.view.dispose(); + } + this.viewModel.dispose(); + } +} + const enum BooleanEventValue { NotSet, False, diff --git a/src/vs/editor/browser/widget/media/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css similarity index 92% rename from src/vs/editor/browser/widget/media/editor.css rename to src/vs/editor/browser/widget/codeEditor/editor.css index 1d60940158a..09c4a32f141 100644 --- a/src/vs/editor/browser/widget/media/editor.css +++ b/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/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts similarity index 59% rename from src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts rename to src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts index 4a5dffa5347..9fb3a8e69c2 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts @@ -4,24 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as objects from 'vs/base/common/objects'; -import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; -import { DiffEditorWidget, IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; -import { ConfigurationChangedEvent, IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ConfigurationChangedEvent, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IThemeService } from 'vs/platform/theme/common/themeService'; export class EmbeddedCodeEditorWidget extends CodeEditorWidget { - private readonly _parentEditor: ICodeEditor; private readonly _overwriteOptions: IEditorOptions; @@ -38,7 +34,7 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget { @INotificationService notificationService: INotificationService, @IAccessibilityService accessibilityService: IAccessibilityService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService ) { super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); @@ -65,45 +61,3 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget { super.updateOptions(this._overwriteOptions); } } - -export class EmbeddedDiffEditorWidget extends DiffEditorWidget { - - private readonly _parentEditor: ICodeEditor; - private readonly _overwriteOptions: IDiffEditorOptions; - - constructor( - domElement: HTMLElement, - options: Readonly, - codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, - parentEditor: ICodeEditor, - @IContextKeyService contextKeyService: IContextKeyService, - @IInstantiationService instantiationService: IInstantiationService, - @ICodeEditorService codeEditorService: ICodeEditorService, - @IAccessibilitySignalService accessibilitySignalService: IAccessibilitySignalService, - @IEditorProgressService editorProgressService: IEditorProgressService, - ) { - super(domElement, parentEditor.getRawOptions(), codeEditorWidgetOptions, contextKeyService, instantiationService, codeEditorService, accessibilitySignalService, editorProgressService); - - this._parentEditor = parentEditor; - this._overwriteOptions = options; - - // Overwrite parent's options - super.updateOptions(this._overwriteOptions); - - this._register(parentEditor.onDidChangeConfiguration(e => this._onParentConfigurationChanged(e))); - } - - getParentEditor(): ICodeEditor { - return this._parentEditor; - } - - private _onParentConfigurationChanged(e: ConfigurationChangedEvent): void { - super.updateOptions(this._parentEditor.getRawOptions()); - super.updateOptions(this._overwriteOptions); - } - - override updateOptions(newOptions: IEditorOptions): void { - objects.mixin(this._overwriteOptions, newOptions, true); - super.updateOptions(this._overwriteOptions); - } -} diff --git a/src/vs/editor/browser/widget/diffEditor/commands.ts b/src/vs/editor/browser/widget/diffEditor/commands.ts new file mode 100644 index 00000000000..cdfff8f57fe --- /dev/null +++ b/src/vs/editor/browser/widget/diffEditor/commands.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 { 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'; +import { URI } from 'vs/base/common/uri'; + +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 Action2 { + constructor() { + super({ + id: 'diffEditor.revert', + title: localize2('revert', 'Revert'), + f1: false, + category: diffEditorCategory, + }); + } + + run(accessor: ServicesAccessor, arg: DiffEditorSelectionHunkToolbarContext): unknown { + const diffEditor = findDiffEditor(accessor, arg.originalUri, arg.modifiedUri); + 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 findDiffEditor(accessor: ServicesAccessor, originalUri: URI, modifiedUri: URI): IDiffEditor | null { + const codeEditorService = accessor.get(ICodeEditorService); + const diffEditors = codeEditorService.listDiffEditors(); + + return diffEditors.find(diffEditor => { + const modified = diffEditor.getModifiedEditor(); + const original = diffEditor.getOriginalEditor(); + + return modified && modified.getModel()?.uri.toString() === modifiedUri.toString() + && original && original.getModel()?.uri.toString() === originalUri.toString(); + }) || null; +} + +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/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index 79d6509b225..2abc8e74bad 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/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/codeEditorWidget'; +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/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index ac5b8886ac3..23a75bac47d 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -12,7 +12,7 @@ import { IObservable, autorun, derived, derivedWithStore, observableFromEvent, o import { ThemeIcon } from 'vs/base/common/themables'; import { assertIsDefined } from 'vs/base/common/types'; import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { diffDeleteDecoration, diffRemoveIcon } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; import { DiffEditorViewModel, DiffMapping } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; @@ -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/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/inlineDiffDeletedCodeMargin.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/inlineDiffDeletedCodeMargin.ts index ced12b99f7b..f6d88c3afe7 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/inlineDiffDeletedCodeMargin.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/inlineDiffDeletedCodeMargin.ts @@ -10,7 +10,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { isIOS } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; diff --git a/src/vs/editor/browser/widget/diffEditor/delegatingEditorImpl.ts b/src/vs/editor/browser/widget/diffEditor/delegatingEditorImpl.ts index ff3b66c5e09..96a85f35eef 100644 --- a/src/vs/editor/browser/widget/diffEditor/delegatingEditorImpl.ts +++ b/src/vs/editor/browser/widget/diffEditor/delegatingEditorImpl.ts @@ -5,7 +5,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { IPosition, Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts b/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts index 2cc7ffacdc4..75017bce4c3 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditor.contribution.ts +++ b/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/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index 1db9c84ce24..f5bbdb1b43f 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IObservable, ISettableObservable, derived, observableValue } from 'vs/base/common/observable'; +import { IObservable, ISettableObservable, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { Constants } from 'vs/base/common/uint'; import { diffEditorDefaultOptions } from 'vs/editor/common/config/diffEditor'; import { IDiffEditorBaseOptions, IDiffEditorOptions, IEditorOptions, ValidDiffEditorBaseOptions, clampedFloat, clampedInt, boolean as validateBooleanOption, stringSet as validateStringSetOption } from 'vs/editor/common/config/editorOptions'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export class DiffEditorOptions { private readonly _options: ISettableObservable, { changedOptions: IDiffEditorOptions }>; @@ -15,8 +16,11 @@ export class DiffEditorOptions { private readonly _diffEditorWidth = observableValue(this, 0); + private readonly _screenReaderMode = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + constructor( options: Readonly, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { const optionsCopy = { ...options, ...validateDiffEditorOptions(options, diffEditorDefaultOptions) }; this._options = observableValue(this, optionsCopy); @@ -28,16 +32,19 @@ export class DiffEditorOptions { public readonly renderOverviewRuler = derived(this, reader => this._options.read(reader).renderOverviewRuler); public readonly renderSideBySide = derived(this, reader => this._options.read(reader).renderSideBySide - && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader)) + && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader) && !this._screenReaderMode.read(reader)) ); 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 +106,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/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 6b57de66397..7e5ac2ea408 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -16,22 +16,23 @@ import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from 'vs/edi import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { AccessibleDiffViewer, AccessibleDiffViewerModelFromEditors } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { DiffEditorDecorations } from 'vs/editor/browser/widget/diffEditor/components/diffEditorDecorations'; import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditor/components/diffEditorSash'; -import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature'; import { DiffEditorViewZones } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones'; +import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature'; import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; +import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, bindContextKey, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; 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 { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; @@ -40,11 +41,11 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; -import { DelegatingEditor } from './delegatingEditorImpl'; import { DiffEditorEditors } from './components/diffEditorEditors'; +import { DelegatingEditor } from './delegatingEditorImpl'; import { DiffEditorOptions } from './diffEditorOptions'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel'; -import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; +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( @@ -111,7 +117,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension)); this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false); - this._options = new DiffEditorOptions(options); + this._options = this._instantiationService.createInstance(DiffEditorOptions, options); this._register(autorun(reader => { this._options.setWidth(this._rootSizeObserver.width.read(reader)); })); @@ -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; - this._editors.original.layout({ width: originalWidthWithoutMovedBlockLines, height }, true); - this._editors.modified.layout({ width: modifiedWidth, height }, true); + 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); + + 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(), @@ -477,12 +529,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { }; } - revert(diff: DetailedLineRangeMapping): void { - if (diff.innerChanges) { - this.revertRangeMappings(diff.innerChanges); - return; - } - + revert(diff: LineRangeMapping): void { const model = this._diffModel.get(); if (!model || !model.isDiffUpToDate.get()) { return; } @@ -613,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/src/vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget.ts new file mode 100644 index 00000000000..9156c17ead3 --- /dev/null +++ b/src/vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import * as objects from 'vs/base/common/objects'; +import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { DiffEditorWidget, IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { ConfigurationChangedEvent, IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +export class EmbeddedDiffEditorWidget extends DiffEditorWidget { + + private readonly _parentEditor: ICodeEditor; + private readonly _overwriteOptions: IDiffEditorOptions; + + constructor( + domElement: HTMLElement, + options: Readonly, + codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, + parentEditor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @IAccessibilitySignalService accessibilitySignalService: IAccessibilitySignalService, + @IEditorProgressService editorProgressService: IEditorProgressService + ) { + super(domElement, parentEditor.getRawOptions(), codeEditorWidgetOptions, contextKeyService, instantiationService, codeEditorService, accessibilitySignalService, editorProgressService); + + this._parentEditor = parentEditor; + this._overwriteOptions = options; + + // Overwrite parent's options + super.updateOptions(this._overwriteOptions); + + this._register(parentEditor.onDidChangeConfiguration(e => this._onParentConfigurationChanged(e))); + } + + getParentEditor(): ICodeEditor { + return this._parentEditor; + } + + private _onParentConfigurationChanged(e: ConfigurationChangedEvent): void { + super.updateOptions(this._parentEditor.getRawOptions()); + super.updateOptions(this._overwriteOptions); + } + + override updateOptions(newOptions: IEditorOptions): void { + objects.mixin(this._overwriteOptions, newOptions, true); + super.updateOptions(this._overwriteOptions); + } +} diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts new file mode 100644 index 00000000000..8410b6eb85b --- /dev/null +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -0,0 +1,293 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +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, + model.model.original.uri, + model.model.modified.uri, + )]; + } + + const currentDiff = this._currentDiff.read(reader); + + return diffs.mappings.map(m => new DiffGutterItem( + m.lineRangeMapping.withInnerChangesFromLineRanges(), + m.lineRangeMapping === currentDiff?.lineRangeMapping, + MenuId.DiffEditorHunkToolbar, + undefined, + model.model.original.uri, + model.model.modified.uri, + )); + }, + 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, + public readonly originalUri: URI, + public readonly modifiedUri: URI, + ) { + } + 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 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 item = this._item.get(); + const mapping = item.mapping; + return { + mapping, + originalWithModifiedChanges: gutter.computeStagedValue(mapping), + originalUri: item.originalUri, + modifiedUri: item.modifiedUri, + } 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; + + modifiedUri: URI; + originalUri: URI; +} diff --git a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts index f2eb90c6d2f..248f2b46d81 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts @@ -59,27 +59,25 @@ export class HideUnchangedRegionsFeature extends Disposable { super(); this._register(this._editors.original.onDidChangeCursorPosition(e => { - if (e.reason === CursorChangeReason.Explicit) { - const m = this._diffModel.get(); - transaction(tx => { - for (const s of this._editors.original.getSelections() || []) { - m?.ensureOriginalLineIsVisible(s.getStartPosition().lineNumber, RevealPreference.FromCloserSide, tx); - m?.ensureOriginalLineIsVisible(s.getEndPosition().lineNumber, RevealPreference.FromCloserSide, tx); - } - }); - } + if (e.reason === CursorChangeReason.ContentFlush) { return; } + const m = this._diffModel.get(); + transaction(tx => { + for (const s of this._editors.original.getSelections() || []) { + m?.ensureOriginalLineIsVisible(s.getStartPosition().lineNumber, RevealPreference.FromCloserSide, tx); + m?.ensureOriginalLineIsVisible(s.getEndPosition().lineNumber, RevealPreference.FromCloserSide, tx); + } + }); })); this._register(this._editors.modified.onDidChangeCursorPosition(e => { - if (e.reason === CursorChangeReason.Explicit) { - const m = this._diffModel.get(); - transaction(tx => { - for (const s of this._editors.modified.getSelections() || []) { - m?.ensureModifiedLineIsVisible(s.getStartPosition().lineNumber, RevealPreference.FromCloserSide, tx); - m?.ensureModifiedLineIsVisible(s.getEndPosition().lineNumber, RevealPreference.FromCloserSide, tx); - } - }); - } + if (e.reason === CursorChangeReason.ContentFlush) { return; } + const m = this._diffModel.get(); + transaction(tx => { + for (const s of this._editors.modified.getSelections() || []) { + m?.ensureModifiedLineIsVisible(s.getStartPosition().lineNumber, RevealPreference.FromCloserSide, tx); + m?.ensureModifiedLineIsVisible(s.getEndPosition().lineNumber, RevealPreference.FromCloserSide, tx); + } + }); })); const unchangedRegions = this._diffModel.map((m, reader) => { @@ -349,9 +347,13 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { didMove = didMove || Math.abs(delta) > 2; const lineDelta = Math.round(delta / editor.getOption(EditorOption.lineHeight)); const newVal = Math.max(0, Math.min(cur - lineDelta, this._unchangedRegion.getMaxVisibleLineCountBottom())); - const top = editor.getTopForLineNumber(this._unchangedRegionRange.endLineNumberExclusive); + const top = this._unchangedRegionRange.endLineNumberExclusive > editor.getModel()!.getLineCount() + ? editor.getContentHeight() + : editor.getTopForLineNumber(this._unchangedRegionRange.endLineNumberExclusive); this._unchangedRegion.visibleLineCountBottom.set(newVal, undefined); - const top2 = editor.getTopForLineNumber(this._unchangedRegionRange.endLineNumberExclusive); + const top2 = this._unchangedRegionRange.endLineNumberExclusive > editor.getModel()!.getLineCount() + ? editor.getContentHeight() + : editor.getTopForLineNumber(this._unchangedRegionRange.endLineNumberExclusive); editor.setScrollTop(editor.getScrollTop() + (top2 - top)); }); diff --git a/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts index bd1997491e1..8141cd9452c 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts @@ -10,7 +10,7 @@ import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; import { Color } from 'vs/base/common/color'; import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, autorun, autorunWithStore, derived, observableFromEvent, observableSignalFromEvent } from 'vs/base/common/observable'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { appendRemoveOnDispose } from 'vs/editor/browser/widget/diffEditor/utils'; diff --git a/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts index c82167c3d13..b2a7d382320 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts @@ -15,7 +15,7 @@ import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEdi import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; import { Range } from 'vs/editor/common/core/range'; -import { RangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { GlyphMarginLane } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; @@ -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; } @@ -62,7 +62,7 @@ export class RevertButtonsFeature extends Disposable { const btn = store.add(new RevertButton( m.lineRangeMapping.modified.startLineNumber, this._widget, - m.lineRangeMapping.innerChanges, + m.lineRangeMapping, false )); this._editors.modified.addGlyphMarginWidget(btn); @@ -122,7 +122,7 @@ export class RevertButton extends Disposable implements IGlyphMarginWidget { constructor( private readonly _lineNumber: number, private readonly _widget: DiffEditorWidget, - private readonly _diffs: RangeMapping[], + private readonly _diffs: RangeMapping[] | LineRangeMapping, private readonly _revertSelection: boolean, ) { super(); @@ -142,7 +142,11 @@ export class RevertButton extends Disposable implements IGlyphMarginWidget { })); this._register(addDisposableListener(this._domNode, EventType.CLICK, (e) => { - this._widget.revertRangeMappings(this._diffs); + if (this._diffs instanceof LineRangeMapping) { + this._widget.revert(this._diffs); + } else { + this._widget.revertRangeMappings(this._diffs); + } e.stopPropagation(); e.preventDefault(); })); diff --git a/src/vs/editor/browser/widget/diffEditor/style.css b/src/vs/editor/browser/widget/diffEditor/style.css index 032ff0f19d7..49ad115e36b 100644 --- a/src/vs/editor/browser/widget/diffEditor/style.css +++ b/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/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index b71da8bb55d..a1e263948f2 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/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/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts new file mode 100644 index 00000000000..1c3341a73ef --- /dev/null +++ b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * 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 = gutterItem.range.isEmpty + // Don't trust that `getBottomForLineNumber` for the previous line equals `getTopForLineNumber` for the current one. + ? top + : (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/src/vs/editor/browser/widget/multiDiffEditorWidget/colors.ts b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts similarity index 100% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/colors.ts rename to src/vs/editor/browser/widget/multiDiffEditor/colors.ts diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts similarity index 98% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate.ts rename to src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index f5433ba99d7..c6f3ca9de9e 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -10,8 +10,8 @@ import { autorun, derived, observableFromEvent } from 'vs/base/common/observable import { IObservable, globalTransaction, observableValue } from 'vs/base/common/observableInternal/base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; -import { DocumentDiffItemViewModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel'; -import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory'; +import { DocumentDiffItemViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; +import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -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/src/vs/editor/browser/widget/multiDiffEditorWidget/model.ts b/src/vs/editor/browser/widget/multiDiffEditor/model.ts similarity index 100% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/model.ts rename to src/vs/editor/browser/widget/multiDiffEditor/model.ts diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts similarity index 95% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel.ts rename to src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 3a26a82c526..1fec3c57c96 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -8,7 +8,7 @@ import { observableFromEvent, observableValue, transaction } from 'vs/base/commo import { mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; -import { IDocumentDiffItem, IMultiDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditorWidget/model'; +import { IDocumentDiffItem, IMultiDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditor/model'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Selection } from 'vs/editor/common/core/selection'; import { IDiffEditorViewModel } from 'vs/editor/common/editorCommon'; @@ -87,7 +87,7 @@ export class DocumentDiffItemViewModel extends Disposable { }; } - const options = new DiffEditorOptions(updateOptions(this.entry.value!.options || {})); + const options = this._instantiationService.createInstance(DiffEditorOptions, updateOptions(this.entry.value!.options || {})); if (this.entry.value!.onOptionsDidChange) { this._register(this.entry.value!.onOptionsDidChange(() => { options.updateOptions(updateOptions(this.entry.value!.options || {})); diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts similarity index 90% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts rename to src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts index 6867bb84ac7..496b002489b 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts @@ -7,13 +7,13 @@ import { Dimension } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { derived, derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable'; import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; -import { IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditorWidget/model'; -import { IMultiDiffEditorViewState, IMultiDiffResource, MultiDiffEditorWidgetImpl } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl'; +import { IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; +import { IMultiDiffEditorViewState, IMultiDiffResourceId, MultiDiffEditorWidgetImpl } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import './colors'; -import { DiffEditorItemTemplate } from 'vs/editor/browser/widget/multiDiffEditorWidget/diffEditorItemTemplate'; -import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory'; +import { DiffEditorItemTemplate } from 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate'; +import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { IDiffEditor } from 'vs/editor/common/editorCommon'; @@ -46,7 +46,7 @@ export class MultiDiffEditorWidget extends Disposable { this._register(recomputeInitiallyAndOnChange(this._widgetImpl)); } - public reveal(resource: IMultiDiffResource, options?: RevealOptions): void { + public reveal(resource: IMultiDiffResourceId, options?: RevealOptions): void { this._widgetImpl.get().reveal(resource, options); } diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts similarity index 96% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts rename to src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index 36a568f2524..bad68c115c1 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -14,8 +14,8 @@ import { URI } from 'vs/base/common/uri'; import 'vs/css!./style'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ObservableElementSizeObserver } from 'vs/editor/browser/widget/diffEditor/utils'; -import { RevealOptions } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget'; -import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory'; +import { RevealOptions } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget'; +import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -28,6 +28,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate'; import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { ObjectPool } from './objectPool'; +import { BugIndicatingError } from 'vs/base/common/errors'; export class MultiDiffEditorWidgetImpl extends Disposable { private readonly _elements = h('div.monaco-component.multiDiffEditor', [ @@ -191,15 +192,15 @@ export class MultiDiffEditorWidgetImpl extends Disposable { this._scrollableElement.setScrollPosition({ scrollLeft: scrollState.left, scrollTop: scrollState.top }); } - public reveal(resource: IMultiDiffResource, options?: RevealOptions): void { + public reveal(resource: IMultiDiffResourceId, options?: RevealOptions): void { const viewItems = this._viewItems.get(); - let searchCallback: (item: VirtualizedViewItem) => boolean; - if ('original' in resource) { - searchCallback = (item) => item.viewModel.originalUri?.toString() === resource.original.toString(); - } else { - searchCallback = (item) => item.viewModel.modifiedUri?.toString() === resource.modified.toString(); + const index = viewItems.findIndex( + (item) => item.viewModel.originalUri?.toString() === resource.original?.toString() + && item.viewModel.modifiedUri?.toString() === resource.modified?.toString() + ); + if (index === -1) { + throw new BugIndicatingError('Resource not found in diff editor'); } - const index = viewItems.findIndex(searchCallback); let scrollTop = 0; for (let i = 0; i < index; i++) { scrollTop += viewItems[i].contentHeight.get() + this._spaceBetweenPx; @@ -323,12 +324,12 @@ export interface IMultiDiffEditorOptions extends ITextEditorOptions { export interface IMultiDiffEditorOptionsViewState { revealData?: { - resource: IMultiDiffResource; + resource: IMultiDiffResourceId; range?: IRange; }; } -export type IMultiDiffResource = { original: URI } | { modified: URI }; +export type IMultiDiffResourceId = { original: URI | undefined; modified: URI | undefined }; class VirtualizedViewItem extends Disposable { private readonly _templateRef = this._register(disposableObservableValue | undefined>(this, undefined)); diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/objectPool.ts b/src/vs/editor/browser/widget/multiDiffEditor/objectPool.ts similarity index 100% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/objectPool.ts rename to src/vs/editor/browser/widget/multiDiffEditor/objectPool.ts diff --git a/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css b/src/vs/editor/browser/widget/multiDiffEditor/style.css similarity index 99% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/style.css rename to src/vs/editor/browser/widget/multiDiffEditor/style.css index 44f0703d580..c540a46b3f1 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/style.css +++ b/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/src/vs/editor/browser/widget/multiDiffEditorWidget/utils.ts b/src/vs/editor/browser/widget/multiDiffEditor/utils.ts similarity index 81% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/utils.ts rename to src/vs/editor/browser/widget/multiDiffEditor/utils.ts index 43449e5827d..be9240267e1 100644 --- a/src/vs/editor/browser/widget/multiDiffEditorWidget/utils.ts +++ b/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/src/vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory.ts b/src/vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory.ts similarity index 100% rename from src/vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory.ts rename to src/vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory.ts diff --git a/src/vs/editor/common/commands/trimTrailingWhitespaceCommand.ts b/src/vs/editor/common/commands/trimTrailingWhitespaceCommand.ts index ebceaddc032..343a5739f19 100644 --- a/src/vs/editor/common/commands/trimTrailingWhitespaceCommand.ts +++ b/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/src/vs/editor/common/config/diffEditor.ts b/src/vs/editor/common/config/diffEditor.ts index 2a62c479848..2d0a312357e 100644 --- a/src/vs/editor/common/config/diffEditor.ts +++ b/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/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 3b22985f00d..ab2b8cc70c6 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/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/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index ec500688918..d839d258315 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/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 `~!@#$%^&*()-=+[{]}\\|;:\'",.<>/? @@ -817,6 +824,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. @@ -2763,7 +2774,7 @@ export type EditorLightbulbOptions = Readonly> class EditorLightbulb extends BaseEditorOption { constructor() { - const defaults: EditorLightbulbOptions = { enabled: ShowLightbulbIconMode.OnCode }; + const defaults: EditorLightbulbOptions = { enabled: ShowLightbulbIconMode.On }; super( EditorOption.lightbulb, 'lightbulb', defaults, { @@ -3062,6 +3073,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; } /** @@ -3083,6 +3106,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 @@ -5300,6 +5401,7 @@ export const enum EditorOption { useShadowDOM, useTabStops, wordBreak, + wordSegmenterLocales, wordSeparators, wordWrap, wordWrapBreakAfterCharacters, @@ -5557,7 +5659,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( @@ -6029,7 +6131,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', @@ -6043,6 +6145,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/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index 95b738b7b6f..88e0419268f 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -20,6 +20,10 @@ export const editorSymbolHighlightBorder = registerColor('editor.symbolHighlight export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('caret', 'Color of the editor cursor.')); export const editorCursorBackground = registerColor('editorCursor.background', null, nls.localize('editorCursorBackground', 'The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.')); +export const editorMultiCursorPrimaryForeground = registerColor('editorMultiCursor.primary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorPrimaryForeground', 'Color of the primary editor cursor when multiple cursors are present.')); +export const editorMultiCursorPrimaryBackground = registerColor('editorMultiCursor.primary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorPrimaryBackground', 'The background color of the primary editor cursor when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.')); +export const editorMultiCursorSecondaryForeground = registerColor('editorMultiCursor.secondary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorSecondaryForeground', 'Color of secondary editor cursors when multiple cursors are present.')); +export const editorMultiCursorSecondaryBackground = registerColor('editorMultiCursor.secondary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorSecondaryBackground', 'The background color of secondary editor cursors when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.')); export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hcDark: '#e3e4e229', hcLight: '#CCCCCC' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.')); export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hcDark: Color.white, hcLight: '#292929' }, nls.localize('editorLineNumbers', 'Color of editor line numbers.')); diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index da150f47952..1cbe63ceba1 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/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/src/vs/editor/common/core/positionToOffset.ts b/src/vs/editor/common/core/positionToOffset.ts new file mode 100644 index 00000000000..484c0a3265f --- /dev/null +++ b/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/src/vs/editor/common/core/rangeMapping.ts b/src/vs/editor/common/core/rangeMapping.ts new file mode 100644 index 00000000000..379e046357d --- /dev/null +++ b/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/src/vs/editor/common/core/textEdit.ts b/src/vs/editor/common/core/textEdit.ts new file mode 100644 index 00000000000..e353361d953 --- /dev/null +++ b/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/src/vs/editor/common/core/textLength.ts b/src/vs/editor/common/core/textLength.ts new file mode 100644 index 00000000000..632895c55fd --- /dev/null +++ b/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/src/vs/editor/common/core/wordCharacterClassifier.ts b/src/vs/editor/common/core/wordCharacterClassifier.ts index 638ff3ac26a..b984c272657 100644 --- a/src/vs/editor/common/core/wordCharacterClassifier.ts +++ b/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/src/vs/editor/common/cursor/cursor.ts b/src/vs/editor/common/cursor/cursor.ts index a5be0b58c33..4df16ec8db2 100644 --- a/src/vs/editor/common/cursor/cursor.ts +++ b/src/vs/editor/common/cursor/cursor.ts @@ -136,7 +136,7 @@ export class CursorsController extends Disposable { this._columnSelectData = columnSelectData; } - public revealPrimary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, minimalReveal: boolean, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { + public revealAll(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, minimalReveal: boolean, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { const viewPositions = this._cursors.getViewPositions(); let revealViewRange: Range | null = null; @@ -150,6 +150,12 @@ export class CursorsController extends Disposable { eventsCollector.emitViewEvent(new ViewRevealRangeRequestEvent(source, minimalReveal, revealViewRange, revealViewSelections, verticalType, revealHorizontal, scrollType)); } + public revealPrimary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, minimalReveal: boolean, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { + const primaryCursor = this._cursors.getPrimaryCursor(); + const revealViewSelections = [primaryCursor.viewState.selection]; + eventsCollector.emitViewEvent(new ViewRevealRangeRequestEvent(source, minimalReveal, null, revealViewSelections, verticalType, revealHorizontal, scrollType)); + } + public saveState(): editorCommon.ICursorState[] { const result: editorCommon.ICursorState[] = []; @@ -212,7 +218,7 @@ export class CursorsController extends Disposable { } this.setStates(eventsCollector, 'restoreState', CursorChangeReason.NotSet, CursorState.fromModelSelections(desiredSelections)); - this.revealPrimary(eventsCollector, 'restoreState', false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Immediate); + this.revealAll(eventsCollector, 'restoreState', false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Immediate); } public onModelContentChanged(eventsCollector: ViewModelEventsCollector, event: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void { @@ -252,7 +258,7 @@ export class CursorsController extends Disposable { if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) { const cursorState = CursorState.fromModelSelections(e.resultingSelection); if (this.setStates(eventsCollector, 'modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState)) { - this.revealPrimary(eventsCollector, 'modelChange', false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); + this.revealAll(eventsCollector, 'modelChange', false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); } } else { const selectionsFromMarkers = this._cursors.readSelectionFromMarkers(); @@ -519,7 +525,7 @@ export class CursorsController extends Disposable { this._cursors.startTrackingSelections(); this._validateAutoClosedActions(); if (this._emitStateChangedIfNecessary(eventsCollector, source, cursorChangeReason, oldState, false)) { - this.revealPrimary(eventsCollector, source, false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); + this.revealAll(eventsCollector, source, false, VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); } } diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index e71f02a960e..ffa80cbb63c 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/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/src/vs/editor/common/cursor/cursorWordOperations.ts b/src/vs/editor/common/cursor/cursorWordOperations.ts index 8a3f98d37c2..b16172cc89a 100644 --- a/src/vs/editor/common/cursor/cursorWordOperations.ts +++ b/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/src/vs/editor/common/cursorCommon.ts b/src/vs/editor/common/cursorCommon.ts index 13b95ad1299..c5411aa8539 100644 --- a/src/vs/editor/common/cursorCommon.ts +++ b/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/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts index e2de212aa40..b7c34e07604 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts @@ -13,11 +13,11 @@ import { DateTimeout, ITimeout, InfiniteTimeout, SequenceDiff } from 'vs/editor/ import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm'; import { computeMovedLines } from 'vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines'; -import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs, removeShortMatches } from 'vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations'; +import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShortMatches, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs } from 'vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations'; +import { LineSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/lineSequence'; +import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from 'vs/editor/common/diff/linesDiffComputer'; import { DetailedLineRangeMapping, RangeMapping } from '../rangeMapping'; -import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; -import { LineSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/lineSequence'; export class DefaultLinesDiffComputer implements ILinesDiffComputer { private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing(); @@ -256,8 +256,11 @@ export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[], or } assertFn(() => { - if (!dontAssertStartLine) { - if (changes.length > 0 && changes[0].original.startLineNumber !== changes[0].modified.startLineNumber) { + if (!dontAssertStartLine && changes.length > 0) { + if (changes[0].modified.startLineNumber !== changes[0].original.startLineNumber) { + return false; + } + if (modifiedLines.length - changes[changes.length - 1].modified.endLineNumberExclusive !== originalLines.length - changes[changes.length - 1].original.endLineNumberExclusive) { return false; } } diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts index 08efd578136..fddfb1e0c61 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts @@ -247,7 +247,7 @@ export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSe while (equalMappings.length > 0) { const next = equalMappings[0]; - const intersects = next.seq1Range.intersects(w1) || next.seq2Range.intersects(w2); + const intersects = next.seq1Range.intersects(w.seq1Range) || next.seq2Range.intersects(w.seq2Range); if (!intersects) { break; } diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index d00f0061698..810df11032f 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/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. @@ -112,6 +118,12 @@ export class DetailedLineRangeMapping extends LineRangeMapping { public override flip(): DetailedLineRangeMapping { return new DetailedLineRangeMapping(this.modified, this.original, this.innerChanges?.map(c => c.flip())); } + + public withInnerChangesFromLineRanges(): DetailedLineRangeMapping { + return new DetailedLineRangeMapping(this.original, this.modified, [ + new RangeMapping(this.original.toExclusiveRange(), this.modified.toExclusiveRange()), + ]); + } } /** diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 480c1250bae..57b9cde6268 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/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/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index a38c21c4717..2311fbd3a85 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/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/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 0f9a3e3bfd2..09419c4baf7 100644 --- a/src/vs/editor/common/languages.ts +++ b/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; } /** @@ -1743,6 +1777,14 @@ export interface CommentInfo { commentingRanges: CommentingRanges; } + +/** + * @internal + */ +export interface CommentingRangeResourceHint { + schemes: readonly string[]; +} + /** * @internal */ @@ -1765,6 +1807,14 @@ export enum CommentThreadState { Resolved = 1 } +/** + * @internal + */ +export enum CommentThreadApplicability { + Current = 0, + Outdated = 1 +} + /** * @internal */ @@ -1802,6 +1852,7 @@ export interface CommentThread { initialCollapsibleState?: CommentThreadCollapsibleState; onDidChangeInitialCollapsibleState: Event; state?: CommentThreadState; + applicability?: CommentThreadApplicability; canReply: boolean; input?: CommentInput; onDidChangeInput: Event; @@ -1892,7 +1943,7 @@ export interface PendingCommentThread { body: string; range: IRange | undefined; uri: URI; - owner: string; + uniqueOwner: string; isReply: boolean; } @@ -2123,13 +2174,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 }; @@ -2143,7 +2195,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/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index ad0d31765ff..e21aa7d600c 100644 --- a/src/vs/editor/common/model.ts +++ b/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/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts index 501aa07c39b..1f95f84df48 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts +++ b/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/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts index 40cb0255688..d41a62233e5 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts +++ b/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/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 7117b8240af..a3e282ba628 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/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/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index 87c4bd8ecf7..81f6cbc5e20 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/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/src/vs/editor/common/model/textModelText.ts b/src/vs/editor/common/model/textModelText.ts new file mode 100644 index 00000000000..0a603fa1ed2 --- /dev/null +++ b/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/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index fdfb6dbe98f..fb1b7364d49 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/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/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 61490912068..804f63c6a28 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/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/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index f03e018cac0..195a870b0af 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/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/src/vs/editor/common/services/editorWorker.ts b/src/vs/editor/common/services/editorWorker.ts index 9e1cca8a460..7e87024cafc 100644 --- a/src/vs/editor/common/services/editorWorker.ts +++ b/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/src/vs/editor/common/services/findSectionHeaders.ts b/src/vs/editor/common/services/findSectionHeaders.ts new file mode 100644 index 00000000000..08bd3709741 --- /dev/null +++ b/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/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 1bb5ee50cac..35852d96852 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -303,27 +303,28 @@ 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, - quickSuggestionsMinimumLength = 149, - tabSuggest = 150 + 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, + quickSuggestionsMinimumLength = 150, + tabSuggest = 151 } /** @@ -648,6 +649,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. */ @@ -742,6 +751,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/src/vs/editor/common/tokenizationTextModelPart.ts b/src/vs/editor/common/tokenizationTextModelPart.ts index 8884b008d98..07eb06f9fdc 100644 --- a/src/vs/editor/common/tokenizationTextModelPart.ts +++ b/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/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index 7bb55aeef6e..71bf9d5b956 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/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/src/vs/editor/common/viewLayout/viewLinesViewportData.ts b/src/vs/editor/common/viewLayout/viewLinesViewportData.ts index 8ddcfddb99d..6e072c52648 100644 --- a/src/vs/editor/common/viewLayout/viewLinesViewportData.ts +++ b/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/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 4f92417e89b..9356bb2ab01 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -87,6 +87,7 @@ export interface IViewModel extends ICursorSimpleModel { setCursorColumnSelectData(columnSelectData: IColumnSelectData): void; getPrevEditOperationType(): EditOperationType; setPrevEditOperationType(type: EditOperationType): void; + revealAllCursors(source: string | null | undefined, revealHorizontal: boolean, minimalReveal?: boolean): void; revealPrimaryCursor(source: string | null | undefined, revealHorizontal: boolean, minimalReveal?: boolean): void; revealTopMostCursor(source: string | null | undefined): void; revealBottomMostCursor(source: string | null | undefined): void; @@ -181,6 +182,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/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index bf152209d83..ea1f1fba62a 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -1069,6 +1069,9 @@ export class ViewModel extends Disposable implements IViewModel { public executeCommands(commands: ICommand[], source?: string | null | undefined): void { this._executeCursorEdit(eventsCollector => this._cursor.executeCommands(eventsCollector, commands, source)); } + public revealAllCursors(source: string | null | undefined, revealHorizontal: boolean, minimalReveal: boolean = false): void { + this._withViewEventsCollector(eventsCollector => this._cursor.revealAll(eventsCollector, source, minimalReveal, viewEvents.VerticalRevealType.Simple, revealHorizontal, ScrollType.Smooth)); + } public revealPrimaryCursor(source: string | null | undefined, revealHorizontal: boolean, minimalReveal: boolean = false): void { this._withViewEventsCollector(eventsCollector => this._cursor.revealPrimary(eventsCollector, source, minimalReveal, viewEvents.VerticalRevealType.Simple, revealHorizontal, ScrollType.Smooth)); } diff --git a/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/src/vs/editor/contrib/codeAction/browser/codeAction.ts index fb6ce190c81..c665dd778fe 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/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/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index 923c9181b7c..ae15cf9bae0 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/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/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 5511dde958b..784be6bebee 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -36,8 +36,9 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types'; -import { CodeActionModel, CodeActionsState } from './codeActionModel'; +import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; +import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; interface IActionShowOptions { @@ -291,7 +292,22 @@ export class CodeActionController extends Disposable implements IEditorContribut if (token.isCancellationRequested) { return; } - return { canPreview: !!action.action.edit?.edits.length }; + + let canPreview = false; + const actionKind = action.action.kind; + + if (actionKind) { + const hierarchicalKind = new HierarchicalKind(actionKind); + const refactorKinds = [ + CodeActionKind.RefactorExtract, + CodeActionKind.RefactorInline, + CodeActionKind.RefactorRewrite + ]; + + canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind)); + } + + return { canPreview: canPreview || !!action.action.edit?.edits.length }; }, onFocus: (action: CodeActionItem | undefined) => { if (action && action.action) { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts b/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts index 373d3a5a7c6..088fcfc9558 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts +++ b/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/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts index b8ebc3074a9..8763487cb1d 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts +++ b/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/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index 547b6c43108..9c7e0c73b76 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/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/src/vs/editor/contrib/codeAction/common/types.ts b/src/vs/editor/contrib/codeAction/common/types.ts index 19a690e23dc..febdfda4d39 100644 --- a/src/vs/editor/contrib/codeAction/common/types.ts +++ b/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/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts index ac919bb6192..c1783a39f11 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts +++ b/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/src/vs/editor/contrib/comment/browser/comment.ts b/src/vs/editor/contrib/comment/browser/comment.ts index 538d6a3ca6c..2a2a4c28360 100644 --- a/src/vs/editor/contrib/comment/browser/comment.ts +++ b/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/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts b/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts index ac498e8a7e4..a5e19fd79e1 100644 --- a/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts +++ b/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/src/vs/editor/contrib/dnd/browser/dnd.ts b/src/vs/editor/contrib/dnd/browser/dnd.ts index d4576e66c8d..9e355410d48 100644 --- a/src/vs/editor/contrib/dnd/browser/dnd.ts +++ b/src/vs/editor/contrib/dnd/browser/dnd.ts @@ -11,7 +11,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import 'vs/css!./dnd'; import { ICodeEditor, IEditorMouseEvent, IMouseTarget, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts index fbdd84d88d4..20ed163b4b2 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts +++ b/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/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 10654d61a52..87cddb00a8d 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/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'; @@ -56,18 +66,25 @@ export class CopyPasteController extends Disposable implements IEditorContributi return editor.getContribution(CopyPasteController.ID); } - private readonly _editor: ICodeEditor; - - private _currentCopyOperation?: { + /** + * Global tracking the last copy operation. + * + * This is shared across all editors so that you can copy and paste between groups. + * + * TODO: figure out how to make this work with multiple windows + */ + private static _currentCopyOperation?: { readonly handle: string; readonly dataTransferPromise: CancelablePromise; }; + 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, @@ -96,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; @@ -204,8 +221,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi return dataTransfer; }); - this._currentCopyOperation?.dataTransferPromise.cancel(); - this._currentCopyOperation = { handle: handle, dataTransferPromise: promise }; + CopyPasteController._currentCopyOperation?.dataTransferPromise.cancel(); + CopyPasteController._currentCopyOperation = { handle: handle, dataTransferPromise: promise }; } private async handlePaste(e: ClipboardEvent) { @@ -246,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; } @@ -268,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()) { @@ -293,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) { @@ -331,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()) { @@ -347,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); @@ -366,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"), @@ -436,8 +476,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi } private async mergeInDataFromCopy(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken): Promise { - if (metadata?.id && this._currentCopyOperation?.handle === metadata.id) { - const toMergeDataTransfer = await this._currentCopyOperation.dataTransferPromise; + if (metadata?.id && CopyPasteController._currentCopyOperation?.handle === metadata.id) { + const toMergeDataTransfer = await CopyPasteController._currentCopyOperation.dataTransferPromise; if (token.isCancellationRequested) { return; } @@ -459,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, @@ -496,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/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 27812236c50..88ffc64d4bd 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/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/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts index 4817431d189..52dc73b8ce2 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts +++ b/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/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 48dae75565c..32e64cff650 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/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/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts index 55d8dff9e7b..81cc89436cf 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts +++ b/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/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts index f41f6866982..3013bcde756 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts +++ b/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/src/vs/editor/contrib/find/browser/findOptionsWidget.ts b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts index 9ebf4167820..007723f6986 100644 --- a/src/vs/editor/contrib/find/browser/findOptionsWidget.ts +++ b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts @@ -13,6 +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 { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export class FindOptionsWidget extends Widget implements IOverlayWidget { @@ -52,9 +53,12 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground), }; + const hoverDelegate = this._register(createInstantHoverDelegate()); + this.caseSensitive = this._register(new CaseSensitiveToggle({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand), isChecked: this._state.matchCase, + hoverDelegate, ...toggleStyles })); this._domNode.appendChild(this.caseSensitive.domNode); @@ -67,6 +71,7 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this.wholeWords = this._register(new WholeWordsToggle({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand), isChecked: this._state.wholeWord, + hoverDelegate, ...toggleStyles })); this._domNode.appendChild(this.wholeWords.domNode); @@ -79,6 +84,7 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this.regex = this._register(new RegexToggle({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand), isChecked: this._state.isRegex, + hoverDelegate, ...toggleStyles })); this._domNode.appendChild(this.regex.domNode); diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index f89e3d81837..8d80095419d 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -43,6 +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/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.')); @@ -1010,10 +1013,14 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._matchesCount.className = 'matchesCount'; this._updateMatchesCount(); + // Create a scoped hover delegate for all find related buttons + const hoverDelegate = this._register(createInstantHoverDelegate()); + // Previous button this._prevBtn = this._register(new SimpleButton({ label: NLS_PREVIOUS_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.PreviousMatchFindAction), icon: findPreviousMatchIcon, + hoverDelegate, onTrigger: () => { assertIsDefined(this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction)).run().then(undefined, onUnexpectedError); } @@ -1023,6 +1030,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._nextBtn = this._register(new SimpleButton({ label: NLS_NEXT_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.NextMatchFindAction), icon: findNextMatchIcon, + hoverDelegate, onTrigger: () => { assertIsDefined(this._codeEditor.getAction(FIND_IDS.NextMatchFindAction)).run().then(undefined, onUnexpectedError); } @@ -1043,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), @@ -1077,6 +1086,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._closeBtn = this._register(new SimpleButton({ label: NLS_CLOSE_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.CloseFindWidgetCommand), icon: widgetClose, + hoverDelegate, onTrigger: () => { this._state.change({ isRevealed: false, searchScope: null }, false); }, @@ -1138,10 +1148,14 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } })); + // Create scoped hover delegate for replace actions + const replaceHoverDelegate = this._register(createInstantHoverDelegate()); + // Replace one button this._replaceBtn = this._register(new SimpleButton({ label: NLS_REPLACE_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.ReplaceOneAction), icon: findReplaceIcon, + hoverDelegate: replaceHoverDelegate, onTrigger: () => { this._controller.replace(); }, @@ -1157,6 +1171,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._replaceAllBtn = this._register(new SimpleButton({ label: NLS_REPLACE_ALL_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.ReplaceAllAction), icon: findReplaceAllIcon, + hoverDelegate: replaceHoverDelegate, onTrigger: () => { this._controller.replaceAll(); } @@ -1299,6 +1314,7 @@ export interface ISimpleButtonOpts { readonly label: string; readonly className?: string; readonly icon?: ThemeIcon; + readonly hoverDelegate?: IHoverDelegate; readonly onTrigger: () => void; readonly onKeyDown?: (e: IKeyboardEvent) => void; } @@ -1321,11 +1337,11 @@ export class SimpleButton extends Widget { } this._domNode = document.createElement('div'); - this._domNode.title = this._opts.label; this._domNode.tabIndex = 0; this._domNode.className = className; this._domNode.setAttribute('role', 'button'); this._domNode.setAttribute('aria-label', this._opts.label); + this._register(setupCustomHover(opts.hoverDelegate ?? getDefaultHoverDelegate('element'), this._domNode, this._opts.label)); this.onclick(this._domNode, (e) => { this._opts.onTrigger(); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts index b968f248904..aa6b7865439 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts @@ -13,7 +13,7 @@ import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/edit import { IActiveCodeEditor, ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorOption, GoToLocationValues } from 'vs/editor/common/config/editorOptions'; import * as corePosition from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts index ec127689cb0..bb76dd67d6c 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts +++ b/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/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index d23e38b81ab..b80e75d47ec 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -16,7 +16,7 @@ import { Schemas } from 'vs/base/common/network'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import 'vs/css!./referencesWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index a14b7d6d5ae..1db0b5adb34 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -9,7 +9,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; import { EditorAutoIndentStrategy, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder, IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -20,123 +20,11 @@ import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; import { IModelService } from 'vs/editor/common/services/model'; -import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils'; +import * as indentUtils from 'vs/editor/contrib/indentation/common/indentUtils'; import * as nls from 'vs/nls'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { normalizeIndentation } from 'vs/editor/common/core/indentation'; import { getGoodIndentForLine, getIndentMetadata } from 'vs/editor/common/languages/autoIndent'; - -export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] { - if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { - // Model is empty - return []; - } - - const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules; - if (!indentationRules) { - return []; - } - - endLineNumber = Math.min(endLineNumber, model.getLineCount()); - - // Skip `unIndentedLinePattern` lines - while (startLineNumber <= endLineNumber) { - if (!indentationRules.unIndentedLinePattern) { - break; - } - - const text = model.getLineContent(startLineNumber); - if (!indentationRules.unIndentedLinePattern.test(text)) { - break; - } - - startLineNumber++; - } - - if (startLineNumber > endLineNumber - 1) { - return []; - } - - const { tabSize, indentSize, insertSpaces } = model.getOptions(); - const shiftIndent = (indentation: string, count?: number) => { - count = count || 1; - return ShiftCommand.shiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); - }; - const unshiftIndent = (indentation: string, count?: number) => { - count = count || 1; - return ShiftCommand.unshiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); - }; - const indentEdits: ISingleEditOperation[] = []; - - // indentation being passed to lines below - let globalIndent: string; - - // Calculate indentation for the first line - // If there is no passed-in indentation, we use the indentation of the first line as base. - const currentLineText = model.getLineContent(startLineNumber); - let adjustedLineContent = currentLineText; - if (inheritedIndent !== undefined && inheritedIndent !== null) { - globalIndent = inheritedIndent; - const oldIndentation = strings.getLeadingWhitespace(currentLineText); - - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { - globalIndent = unshiftIndent(globalIndent); - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - - } - if (currentLineText !== adjustedLineContent) { - indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces))); - } - } else { - globalIndent = strings.getLeadingWhitespace(currentLineText); - } - - // idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`. - let idealIndentForNextLine: string = globalIndent; - - if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { - idealIndentForNextLine = shiftIndent(idealIndentForNextLine); - globalIndent = shiftIndent(globalIndent); - } - else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { - idealIndentForNextLine = shiftIndent(idealIndentForNextLine); - } - - startLineNumber++; - - // Calculate indentation adjustment for all following lines - for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const text = model.getLineContent(lineNumber); - const oldIndentation = strings.getLeadingWhitespace(text); - const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length); - - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { - idealIndentForNextLine = unshiftIndent(idealIndentForNextLine); - globalIndent = unshiftIndent(globalIndent); - } - - if (oldIndentation !== idealIndentForNextLine) { - indentEdits.push(EditOperation.replaceMove(new Selection(lineNumber, 1, lineNumber, oldIndentation.length + 1), normalizeIndentation(idealIndentForNextLine, indentSize, insertSpaces))); - } - - // calculate idealIndentForNextLine - if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) { - // In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines - // but don't change globalIndent and idealIndentForNextLine. - continue; - } else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { - globalIndent = shiftIndent(globalIndent); - idealIndentForNextLine = globalIndent; - } else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { - idealIndentForNextLine = shiftIndent(idealIndentForNextLine); - } else { - idealIndentForNextLine = globalIndent; - } - } - - return indentEdits; -} +import { getReindentEditOperations } from '../common/indentation'; export class IndentationToSpacesAction extends EditorAction { public static readonly ID = 'editor.action.indentationToSpaces'; diff --git a/src/vs/editor/contrib/indentation/browser/indentUtils.ts b/src/vs/editor/contrib/indentation/common/indentUtils.ts similarity index 100% rename from src/vs/editor/contrib/indentation/browser/indentUtils.ts rename to src/vs/editor/contrib/indentation/common/indentUtils.ts diff --git a/src/vs/editor/contrib/indentation/common/indentation.ts b/src/vs/editor/contrib/indentation/common/indentation.ts new file mode 100644 index 00000000000..760a14919fb --- /dev/null +++ b/src/vs/editor/contrib/indentation/common/indentation.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as strings from 'vs/base/common/strings'; +import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; +import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { normalizeIndentation } from 'vs/editor/common/core/indentation'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ITextModel } from 'vs/editor/common/model'; + +export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] { + if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { + // Model is empty + return []; + } + + const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules; + if (!indentationRules) { + return []; + } + + endLineNumber = Math.min(endLineNumber, model.getLineCount()); + + // Skip `unIndentedLinePattern` lines + while (startLineNumber <= endLineNumber) { + if (!indentationRules.unIndentedLinePattern) { + break; + } + + const text = model.getLineContent(startLineNumber); + if (!indentationRules.unIndentedLinePattern.test(text)) { + break; + } + + startLineNumber++; + } + + if (startLineNumber > endLineNumber - 1) { + return []; + } + + const { tabSize, indentSize, insertSpaces } = model.getOptions(); + const shiftIndent = (indentation: string, count?: number) => { + count = count || 1; + return ShiftCommand.shiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); + }; + const unshiftIndent = (indentation: string, count?: number) => { + count = count || 1; + return ShiftCommand.unshiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); + }; + const indentEdits: ISingleEditOperation[] = []; + + // indentation being passed to lines below + let globalIndent: string; + + // Calculate indentation for the first line + // If there is no passed-in indentation, we use the indentation of the first line as base. + const currentLineText = model.getLineContent(startLineNumber); + let adjustedLineContent = currentLineText; + if (inheritedIndent !== undefined && inheritedIndent !== null) { + globalIndent = inheritedIndent; + const oldIndentation = strings.getLeadingWhitespace(currentLineText); + + adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); + if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { + globalIndent = unshiftIndent(globalIndent); + adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); + + } + if (currentLineText !== adjustedLineContent) { + indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces))); + } + } else { + globalIndent = strings.getLeadingWhitespace(currentLineText); + } + + // idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`. + let idealIndentForNextLine: string = globalIndent; + + if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + idealIndentForNextLine = shiftIndent(idealIndentForNextLine); + globalIndent = shiftIndent(globalIndent); + } + else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + idealIndentForNextLine = shiftIndent(idealIndentForNextLine); + } + + startLineNumber++; + + // Calculate indentation adjustment for all following lines + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + const text = model.getLineContent(lineNumber); + const oldIndentation = strings.getLeadingWhitespace(text); + const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length); + + if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { + idealIndentForNextLine = unshiftIndent(idealIndentForNextLine); + globalIndent = unshiftIndent(globalIndent); + } + + if (oldIndentation !== idealIndentForNextLine) { + indentEdits.push(EditOperation.replaceMove(new Selection(lineNumber, 1, lineNumber, oldIndentation.length + 1), normalizeIndentation(idealIndentForNextLine, indentSize, insertSpaces))); + } + + // calculate idealIndentForNextLine + if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) { + // In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines + // but don't change globalIndent and idealIndentForNextLine. + continue; + } else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + globalIndent = shiftIndent(globalIndent); + idealIndentForNextLine = globalIndent; + } else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + idealIndentForNextLine = shiftIndent(idealIndentForNextLine); + } else { + idealIndentForNextLine = globalIndent; + } + } + + return indentEdits; +} diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 91666a05d19..6f125be77d6 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -6,19 +6,28 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; 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, IState, TokenizationRegistry } from 'vs/editor/common/languages'; +import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; +import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { AutoIndentOnPaste, IndentationToSpacesCommand, IndentationToTabsCommand } from 'vs/editor/contrib/indentation/browser/indentation'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { testCommand } from 'vs/editor/test/browser/testCommand'; -import { javascriptIndentationRules } from 'vs/editor/test/common/modes/supports/javascriptIndentationRules'; -import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { goIndentationRules, javascriptIndentationRules, phpIndentationRules, rubyIndentationRules } from 'vs/editor/test/common/modes/supports/indentationRules'; +import { cppOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; + +enum Language { + TypeScript, + Ruby, + PHP, + Go, + CPP +} function testIndentationToSpacesCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void { testCommand(lines, null, selection, (accessor, sel) => new IndentationToSpacesCommand(sel, tabSize), expectedLines, expectedSelection); @@ -28,7 +37,101 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } -suite('Editor Contrib - Indentation to Spaces', () => { +function registerLanguage(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) { + const languageService = instantiationService.get(ILanguageService); + registerLanguageConfiguration(instantiationService, languageId, language, disposables); + disposables.add(languageService.registerLanguage({ id: languageId })); +} + +// TODO@aiday-mar read directly the configuration file +function registerLanguageConfiguration(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) { + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + switch (language) { + case Language.TypeScript: + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['${', '}'], + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + indentationRules: javascriptIndentationRules, + onEnterRules: javascriptOnEnterRules + })); + break; + case Language.Ruby: + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + indentationRules: rubyIndentationRules, + })); + break; + case Language.PHP: + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + indentationRules: phpIndentationRules, + onEnterRules: phpOnEnterRules + })); + break; + case Language.Go: + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + indentationRules: goIndentationRules + })); + break; + case Language.CPP: + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + onEnterRules: cppOnEnterRules + })); + break; + } +} + +function registerTokens(instantiationService: TestInstantiationService, tokens: { startIndex: number; value: number }[][], languageId: string, disposables: DisposableStore) { + let lineIndex = 0; + const languageService = instantiationService.get(ILanguageService); + const tokenizationSupport: ITokenizationSupport = { + getInitialState: () => NullState, + tokenize: undefined!, + tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { + const tokensOnLine = tokens[lineIndex++]; + const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId); + const result = new Uint32Array(2 * tokensOnLine.length); + for (let i = 0; i < tokensOnLine.length; i++) { + result[2 * i] = tokensOnLine[i].startIndex; + result[2 * i + 1] = + ( + (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) + | (tokensOnLine[i].value << MetadataConsts.TOKEN_TYPE_OFFSET) + ); + } + return new EncodedTokenizationResult(result, state); + } + }; + disposables.add(TokenizationRegistry.register(languageId, tokenizationSupport)); +} + +suite('Change Indentation to Spaces - TypeScript/Javascript', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -117,7 +220,7 @@ suite('Editor Contrib - Indentation to Spaces', () => { }); }); -suite('Editor Contrib - Indentation to Tabs', () => { +suite('Change Indentation to Tabs - TypeScript/Javascript', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -202,7 +305,9 @@ suite('Editor Contrib - Indentation to Tabs', () => { }); }); -suite('Editor Contrib - Auto Indent On Paste', () => { +suite('Auto Indent On Paste - TypeScript/JavaScript', () => { + + const languageId = 'ts-test'; let disposables: DisposableStore; setup(() => { @@ -216,95 +321,61 @@ suite('Editor Contrib - Auto Indent On Paste', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('issue #119225: Do not add extra leading space when pasting JSDoc', () => { - const languageId = 'leadingSpacePaste'; + const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(TokenizationRegistry.register(languageId, { - getInitialState: (): IState => NullState, - tokenize: () => { - throw new Error('not implemented'); - }, - tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { - const tokensArr: number[] = []; - if (line.indexOf('*') !== -1) { - tokensArr.push(0); - tokensArr.push(StandardTokenType.Comment << MetadataConsts.TOKEN_TYPE_OFFSET); - } else { - tokensArr.push(0); - tokensArr.push(StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET); - } - const tokens = new Uint32Array(tokensArr.length); - for (let i = 0; i < tokens.length; i++) { - tokens[i] = tokensArr[i]; - } - return new EncodedTokenizationResult(tokens, state); - } - })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - comments: { - lineComment: '//', - blockComment: ['/*', '*/'] - }, - indentationRules: javascriptIndentationRules, - onEnterRules: javascriptOnEnterRules - })); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { const pasteText = [ '/**', ' * JSDoc', ' */', 'function a() {}' ].join('\n'); - + const tokens = [ + [ + { startIndex: 0, value: 1 }, + { startIndex: 3, value: 1 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 2, value: 1 }, + { startIndex: 8, value: 1 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 1, value: 1 }, + { startIndex: 3, value: 0 }, + ], + [ + { startIndex: 0, value: 0 }, + { startIndex: 8, value: 0 }, + { startIndex: 9, value: 0 }, + { startIndex: 10, value: 0 }, + { startIndex: 11, value: 0 }, + { startIndex: 12, value: 0 }, + { startIndex: 13, value: 0 }, + { startIndex: 14, value: 0 }, + { startIndex: 15, value: 0 }, + ] + ]; + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + registerTokens(instantiationService, tokens, languageId, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 4, 16)); assert.strictEqual(model.getValue(), pasteText); }); }); -}); - -suite('Editor Contrib - Keep Indent On Paste', () => { - let disposables: DisposableStore; - - setup(() => { - disposables = new DisposableStore(); - }); - - teardown(() => { - disposables.dispose(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); test('issue #167299: Blank line removes indent', () => { - const languageId = 'blankLineRemovesIndent'; + const model = createTextModel("", languageId, {}); disposables.add(model); + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: javascriptIndentationRules, - onEnterRules: javascriptOnEnterRules - })); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + // no need for tokenization because there are no comments const pasteText = [ '', 'export type IncludeReference =', @@ -319,14 +390,762 @@ suite('Editor Contrib - Keep Indent On Paste', () => { '}' ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); assert.strictEqual(model.getValue(), pasteText); }); }); + + test('issue #29803: do not indent when pasting text with only one line', () => { + + // https://github.com/microsoft/vscode/issues/29803 + + const model = createTextModel([ + 'const linkHandler = new Class(a, b, c,', + ' d)' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 6, 2, 6)); + const text = ', null'; + viewModel.paste(text, true, undefined, 'keyboard'); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + autoIndentOnPasteController.trigger(new Range(2, 6, 2, 11)); + assert.strictEqual(model.getValue(), [ + 'const linkHandler = new Class(a, b, c,', + ' d, null)' + ].join('\n')); + }); + }); + + test('issue #29753: incorrect indentation after comment', () => { + + // https://github.com/microsoft/vscode/issues/29753 + + const model = createTextModel([ + 'class A {', + ' /**', + ' * used only for debug purposes.', + ' */', + ' private _codeInfo: KeyMapping[];', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(5, 24, 5, 34)); + const text = 'IMacLinuxKeyMapping'; + viewModel.paste(text, true, undefined, 'keyboard'); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + autoIndentOnPasteController.trigger(new Range(5, 24, 5, 43)); + assert.strictEqual(model.getValue(), [ + 'class A {', + ' /**', + ' * used only for debug purposes.', + ' */', + ' private _codeInfo: IMacLinuxKeyMapping[];', + '}', + ].join('\n')); + }); + }); + + test('issue #29753: incorrect indentation of header comment', () => { + + // https://github.com/microsoft/vscode/issues/29753 + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const text = [ + '/*----------------', + ' * Copyright (c) ', + ' * Licensed under ...', + ' *-----------------*/', + ].join('\n'); + viewModel.paste(text, true, undefined, 'keyboard'); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + autoIndentOnPasteController.trigger(new Range(1, 1, 4, 22)); + assert.strictEqual(model.getValue(), text); + }); + }); + + // Failing tests found in issues... + + test.skip('issue #181065: Incorrect paste of object within comment', () => { + + // https://github.com/microsoft/vscode/issues/181065 + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + const text = [ + '/**', + ' * @typedef {', + ' * }', + ' */' + ].join('\n'); + const tokens = [ + [ + { startIndex: 0, value: 1 }, + { startIndex: 3, value: 1 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 2, value: 1 }, + { startIndex: 3, value: 1 }, + { startIndex: 11, value: 1 }, + { startIndex: 12, value: 0 }, + { startIndex: 13, value: 0 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 2, value: 0 }, + { startIndex: 3, value: 0 }, + { startIndex: 4, value: 0 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 1, value: 1 }, + { startIndex: 3, value: 0 }, + ] + ]; + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + registerTokens(instantiationService, tokens, languageId, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(1, 1, 4, 4)); + assert.strictEqual(model.getValue(), text); + }); + }); + + test.skip('issue #86301: preserve cursor at inserted indentation level', () => { + + // https://github.com/microsoft/vscode/issues/86301 + + const model = createTextModel([ + '() => {', + '', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(2, 1, 2, 1)); + const text = [ + '() => {', + '', + '}', + '' + ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(2, 1, 5, 1)); + + // notes: + // why is line 3 not indented to the same level as line 2? + // looks like the indentation is inserted correctly at line 5, but the cursor does not appear at the maximum indentation level? + assert.strictEqual(model.getValue(), [ + '() => {', + ' () => {', + ' ', // <- should also be indented + ' }', + ' ', // <- cursor should be at the end of the indentation + '}', + ].join('\n')); + + const selection = viewModel.getSelection(); + assert.deepStrictEqual(selection, new Selection(5, 5, 5, 5)); + }); + }); + + test.skip('issue #85781: indent line with extra white space', () => { + + // https://github.com/microsoft/vscode/issues/85781 + // note: still to determine whether this is a bug or not + + const model = createTextModel([ + '() => {', + ' console.log("a");', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + const text = [ + '() => {', + ' console.log("b")', + '}', + ' ' + ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + // todo@aiday-mar, make sure range is correct, and make test work as in real life + autoIndentOnPasteController.trigger(new Range(2, 5, 5, 6)); + assert.strictEqual(model.getValue(), [ + '() => {', + ' () => {', + ' console.log("b")', + ' }', + ' console.log("a");', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #29589: incorrect indentation of closing brace on paste', () => { + + // https://github.com/microsoft/vscode/issues/29589 + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + const text = [ + 'function makeSub(a,b) {', + 'subsent = sent.substring(a,b);', + 'return subsent;', + '}', + ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + // todo@aiday-mar, make sure range is correct, and make test work as in real life + autoIndentOnPasteController.trigger(new Range(1, 1, 4, 2)); + assert.strictEqual(model.getValue(), [ + 'function makeSub(a,b) {', + ' subsent = sent.substring(a,b);', + ' return subsent;', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #201420: incorrect indentation when first line is comment', () => { + + // https://github.com/microsoft/vscode/issues/201420 + + const model = createTextModel([ + 'function bar() {', + '', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + const tokens = [ + [{ startIndex: 0, value: 0 }, { startIndex: 8, value: 0 }, { startIndex: 9, value: 0 }, { startIndex: 12, value: 0 }, { startIndex: 13, value: 0 }, { startIndex: 14, value: 0 }, { startIndex: 15, value: 0 }, { startIndex: 16, value: 0 }], + [{ startIndex: 0, value: 1 }, { startIndex: 2, value: 1 }, { startIndex: 3, value: 1 }, { startIndex: 10, value: 1 }], + [{ startIndex: 0, value: 0 }, { startIndex: 5, value: 0 }, { startIndex: 6, value: 0 }, { startIndex: 9, value: 0 }, { startIndex: 10, value: 0 }, { startIndex: 11, value: 0 }, { startIndex: 12, value: 0 }, { startIndex: 14, value: 0 }], + [{ startIndex: 0, value: 0 }, { startIndex: 1, value: 0 }] + ]; + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + registerTokens(instantiationService, tokens, languageId, disposables); + + editor.setSelection(new Selection(2, 1, 2, 1)); + const text = [ + '// comment', + 'const foo = 42', + ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(2, 1, 3, 15)); + assert.strictEqual(model.getValue(), [ + 'function bar() {', + ' // comment', + ' const foo = 42', + '}', + ].join('\n')); + }); + }); }); -suite('Editor Contrib - Auto Dedent On Type', () => { +suite('Auto Indent On Type - TypeScript/JavaScript', () => { + + const languageId = "ts-test"; + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + // Failing tests from issues... + + test('issue #208215: indent after arrow function', () => { + + // https://github.com/microsoft/vscode/issues/208215 + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + viewModel.type('const add1 = (n) =>'); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const add1 = (n) =>', + ' ', + ].join('\n')); + }); + }); + + test('issue #208215: indent after arrow function 2', () => { + + // https://github.com/microsoft/vscode/issues/208215 + + const model = createTextModel([ + 'const array = [1, 2, 3, 4, 5];', + 'array.map(', + ' v =>', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(3, 9, 3, 9)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const array = [1, 2, 3, 4, 5];', + 'array.map(', + ' v =>', + ' ' + ].join('\n')); + }); + }); + + test('issue #116843: indent after arrow function', () => { + + // https://github.com/microsoft/vscode/issues/116843 + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + + viewModel.type([ + 'const add1 = (n) =>', + ' n + 1;', + ].join('\n')); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const add1 = (n) =>', + ' n + 1;', + '', + ].join('\n')); + }); + }); + + test('issue #29755: do not add indentation on enter if indentation is already valid', () => { + + //https://github.com/microsoft/vscode/issues/29755 + + const model = createTextModel([ + 'function f() {', + ' const one = 1;', + ' const two = 2;', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(3, 1, 3, 1)); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function f() {', + ' const one = 1;', + '', + ' const two = 2;', + '}', + ].join('\n')); + }); + }); + + test('issue #36090', () => { + + // https://github.com/microsoft/vscode/issues/36090 + + const model = createTextModel([ + 'class ItemCtrl {', + ' getPropertiesByItemId(id) {', + ' return this.fetchItem(id)', + ' .then(item => {', + ' return this.getPropertiesOfItem(item);', + ' });', + ' }', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(7, 6, 7, 6)); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), + [ + 'class ItemCtrl {', + ' getPropertiesByItemId(id) {', + ' return this.fetchItem(id)', + ' .then(item => {', + ' return this.getPropertiesOfItem(item);', + ' });', + ' }', + ' ', + '}', + ].join('\n') + ); + assert.deepStrictEqual(editor.getSelection(), new Selection(8, 5, 8, 5)); + }); + }); + + test('issue #115304: indent block comment onEnter', () => { + + // https://github.com/microsoft/vscode/issues/115304 + + const model = createTextModel([ + '/** */', + 'function f() {}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(1, 4, 1, 4)); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), + [ + '/**', + ' * ', + ' */', + 'function f() {}', + ].join('\n') + ); + assert.deepStrictEqual(editor.getSelection(), new Selection(2, 4, 2, 4)); + }); + }); + + test('issue #43244: indent when lambda arrow function is detected, outdent when end is reached', () => { + + // https://github.com/microsoft/vscode/issues/43244 + + const model = createTextModel([ + 'const array = [1, 2, 3, 4, 5];', + 'array.map(_)' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 12, 2, 12)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const array = [1, 2, 3, 4, 5];', + 'array.map(_', + ' ', + ')' + ].join('\n')); + }); + }); + + test('issue #43244: incorrect indentation after if/for/while without braces', () => { + + // https://github.com/microsoft/vscode/issues/43244 + + const model = createTextModel([ + 'function f() {', + ' if (condition)', + '}' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 19, 2, 19)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function f() {', + ' if (condition)', + ' ', + '}', + ].join('\n')); + + viewModel.type("return;"); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function f() {', + ' if (condition)', + ' return;', + ' ', + '}', + ].join('\n')); + }); + }); + + // Failing tests... + + test.skip('issue #208232: incorrect indentation inside of comments', () => { + + // https://github.com/microsoft/vscode/issues/208232 + + const model = createTextModel([ + '/**', + 'indentation done for {', + '*/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 23, 2, 23)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + '/**', + 'indentation done for {', + '', + '*/' + ].join('\n')); + }); + }); + + test.skip('issue #43244: indent after equal sign is detected', () => { + + // https://github.com/microsoft/vscode/issues/43244 + // issue: Should indent after an equal sign is detected followed by whitespace characters. + // This should be outdented when a semi-colon is detected indicating the end of the assignment. + + // TODO: requires exploring indent/outdent pairs instead + + const model = createTextModel([ + 'const array =' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(1, 14, 1, 14)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const array =', + ' ' + ].join('\n')); + }); + }); + + test.skip('issue #43244: indent after dot detected after object/array signifying a method call', () => { + + // https://github.com/microsoft/vscode/issues/43244 + // issue: When a dot is written, we should detect that this is a method call and indent accordingly + + // TODO: requires exploring indent/outdent pairs instead + + const model = createTextModel([ + 'const array = [1, 2, 3];', + 'array.' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 7, 2, 7)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const array = [1, 2, 3];', + 'array.', + ' ' + ].join('\n')); + }); + }); + + test.skip('issue #43244: indent after dot detected on a subsequent line after object/array signifying a method call', () => { + + // https://github.com/microsoft/vscode/issues/43244 + // issue: When a dot is written, we should detect that this is a method call and indent accordingly + + // TODO: requires exploring indent/outdent pairs instead + + const model = createTextModel([ + 'const array = [1, 2, 3]', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 7, 2, 7)); + viewModel.type("\n", 'keyboard'); + viewModel.type("."); + assert.strictEqual(model.getValue(), [ + 'const array = [1, 2, 3]', + ' .' + ].join('\n')); + }); + }); + + test.skip('issue #43244: keep indentation when methods called on object/array', () => { + + // https://github.com/microsoft/vscode/issues/43244 + // Currently passes, but should pass with all the tests above too + + // TODO: requires exploring indent/outdent pairs instead + + const model = createTextModel([ + 'const array = [1, 2, 3]', + ' .filter(() => true)' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 24, 2, 24)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const array = [1, 2, 3]', + ' .filter(() => true)', + ' ' + ].join('\n')); + }); + }); + + test.skip('issue #43244: keep indentation when chained methods called on object/array', () => { + + // https://github.com/microsoft/vscode/issues/43244 + // When the call chain is not finished yet, and we type a dot, we do not want to change the indentation + + // TODO: requires exploring indent/outdent pairs instead + + const model = createTextModel([ + 'const array = [1, 2, 3]', + ' .filter(() => true)', + ' ' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(3, 5, 3, 5)); + viewModel.type("."); + assert.strictEqual(model.getValue(), [ + 'const array = [1, 2, 3]', + ' .filter(() => true)', + ' .' // here we don't want to increase the indentation because we have chained methods + ].join('\n')); + }); + }); + + test.skip('issue #43244: outdent when a semi-color is detected indicating the end of the assignment', () => { + + // https://github.com/microsoft/vscode/issues/43244 + + // TODO: requires exploring indent/outdent pairs instead + + const model = createTextModel([ + 'const array = [1, 2, 3]', + ' .filter(() => true);' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 25, 2, 25)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const array = [1, 2, 3]', + ' .filter(() => true);', + '' + ].join('\n')); + }); + }); + + + test.skip('issue #40115: keep indentation when added', () => { + + // https://github.com/microsoft/vscode/issues/40115 + + const model = createTextModel('function foo() {}', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + + editor.setSelection(new Selection(1, 17, 1, 17)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function foo() {', + ' ', + '}', + ].join('\n')); + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function foo() {', + ' ', + ' ', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #193875: incorrect indentation on enter', () => { + + // https://github.com/microsoft/vscode/issues/193875 + + const model = createTextModel([ + '{', + ' for(;;)', + ' for(;;) {}', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(3, 14, 3, 14)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + '{', + ' for(;;)', + ' for(;;) {', + ' ', + ' }', + '}', + ].join('\n')); + }); + }); + + // Add tests for: + // https://github.com/microsoft/vscode/issues/88638 + // https://github.com/microsoft/vscode/issues/63388 + // https://github.com/microsoft/vscode/issues/46401 + // https://github.com/microsoft/vscode/issues/174044 +}); + +suite('Auto Indent On Type - Ruby', () => { + + const languageId = "ruby-test"; let disposables: DisposableStore; setup(() => { @@ -340,50 +1159,180 @@ suite('Editor Contrib - Auto Dedent On Type', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('issue #198350: in or when incorrectly match non keywords for Ruby', () => { - const languageId = "ruby"; + + // https://github.com/microsoft/vscode/issues/198350 + const model = createTextModel("", languageId, {}); disposables.add(model); withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - const languageModel = languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - decreaseIndentPattern: /\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif|when|in)\b)/, - increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/, - }, - }); - viewModel.type("def foo\n i", 'keyboard'); - viewModel.type("n", 'keyboard'); - // The 'in' triggers decreaseIndentPattern immediately, which is incorrect - assert.strictEqual(model.getValue(), "def foo\nin"); - languageModel.dispose(); + registerLanguage(instantiationService, languageId, Language.Ruby, disposables); - const improvedLanguageModel = languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - decreaseIndentPattern: /\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif)\b)|((in|when)\s)/, - increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/, - }, - }); - viewModel.model.setValue(""); viewModel.type("def foo\n i"); viewModel.type("n", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\n in"); viewModel.type(" ", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\nin "); - improvedLanguageModel.dispose(); + + viewModel.model.setValue(""); + viewModel.type(" # in"); + assert.strictEqual(model.getValue(), " # in"); + viewModel.type(" ", 'keyboard'); + assert.strictEqual(model.getValue(), " # in "); + }); + }); + + // Failing tests... + + test.skip('issue #199846: in or when incorrectly match non keywords for Ruby', () => { + + // https://github.com/microsoft/vscode/issues/199846 + // explanation: happening because the # is detected probably as a comment + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.Ruby, disposables); + + viewModel.type("method('#foo') do"); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + "method('#foo') do", + " " + ].join('\n')); + }); + }); +}); + +suite('Auto Indent On Type - PHP', () => { + + const languageId = "php-test"; + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #199050: should not indent after { detected in a string', () => { + + // https://github.com/microsoft/vscode/issues/199050 + + const model = createTextModel("$phrase = preg_replace('#(\{1|%s).*#su', '', $phrase);", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.PHP, disposables); + editor.setSelection(new Selection(1, 54, 1, 54)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + "$phrase = preg_replace('#(\{1|%s).*#su', '', $phrase);", + "" + ].join('\n')); + }); + }); +}); + +suite('Auto Indent On Paste - Go', () => { + + const languageId = "go-test"; + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #199050: should not indent after { detected in a string', () => { + + // https://github.com/microsoft/vscode/issues/199050 + + const model = createTextModel([ + 'var s = `', + 'quick brown', + 'fox', + '`', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.Go, disposables); + editor.setSelection(new Selection(3, 1, 3, 1)); + const text = ' '; + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(3, 1, 3, 3)); + assert.strictEqual(model.getValue(), [ + 'var s = `', + 'quick brown', + ' fox', + '`', + ].join('\n')); + }); + }); +}); + +suite('Auto Indent On Type - CPP', () => { + + const languageId = "cpp-test"; + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #178334: incorrect outdent of } when signature spans multiple lines', () => { + + // https://github.com/microsoft/vscode/issues/178334 + + const model = createTextModel([ + 'int WINAPI WinMain(bool instance,', + ' int nshowcmd) {}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + registerLanguage(instantiationService, languageId, Language.CPP, disposables); + editor.setSelection(new Selection(2, 20, 2, 20)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'int WINAPI WinMain(bool instance,', + ' int nshowcmd) {', + ' ', + '}' + ].join('\n')); }); }); }); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts index 42d0404984f..21abd0b7093 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts index 30f622e01fc..e847839c042 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 006c9873c52..996471bde48 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts index 3e38e9e8161..94a8b33d477 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts index 9d91e0ade1e..28052040c32 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts index 8517f24ec85..750eb459829 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/singleTextEdit.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts index 90d53b26c8f..5f98b45033a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 4a9e78238b6..20236aade3b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index 67c56bb3945..648f5940596 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index 4c846025949..e160c3daa01 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/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/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.test.ts deleted file mode 100644 index 16c918fbe18..00000000000 --- a/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/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 49e09d4e4a7..11c24b0b0e6 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/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/src/vs/editor/contrib/inlineEdit/browser/commands.ts b/src/vs/editor/contrib/inlineEdit/browser/commands.ts index 11d6dfa2f22..d7e1f4fe8cb 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/commands.ts +++ b/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/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts index da0fd80b543..298fcd4452e 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts @@ -176,7 +176,7 @@ export class GhostTextWidget extends Disposable { } else { const lines = uiState.range.endLineNumber - uiState.range.startLineNumber; - for (let i = 0; i <= lines; i++) { + for (let i = 0; i < lines; i++) { const line = uiState.range.startLineNumber + i; const firstNonWhitespace = uiState.targetTextModel.getLineFirstNonWhitespaceColumn(line); const lastNonWhitespace = uiState.targetTextModel.getLineLastNonWhitespaceColumn(line); diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 8355ef312cf..4e0c10eb335 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/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/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts index 73824bd5e6c..59553805863 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts +++ b/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/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts b/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts index 0e773d81809..f2b2a2df9b5 100644 --- a/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts +++ b/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts @@ -39,7 +39,7 @@ export class ExpandLineSelectionAction extends EditorAction { CursorChangeReason.Explicit, CursorMoveCommands.expandLineSelection(viewModel, viewModel.getCursorStates()) ); - viewModel.revealPrimaryCursor(args.source, true); + viewModel.revealAllCursors(args.source, true); } } diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 74d7849587e..45b3fafba71 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/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/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 8db880a15ad..68614a2f432 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -13,7 +13,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { CompleteEnterAction, IndentAction } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; -import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils'; +import * as indentUtils from 'vs/editor/contrib/indentation/common/indentUtils'; import { getGoodIndentForLine, getIndentMetadata, IIndentConverter, IVirtualModel } from 'vs/editor/common/languages/autoIndent'; import { getEnterAction } from 'vs/editor/common/languages/enterAction'; diff --git a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts index 3df2a1f682c..5425697a2e4 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts +++ b/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/src/vs/editor/contrib/links/browser/links.ts b/src/vs/editor/contrib/links/browser/links.ts index 9527453bf32..073c85c85ea 100644 --- a/src/vs/editor/contrib/links/browser/links.ts +++ b/src/vs/editor/contrib/links/browser/links.ts @@ -237,9 +237,9 @@ export class LinkDetector extends Disposable implements IEditorContribution { const fsPath = resources.originalFSPath(parsedUri); let relativePath: string | null = null; - if (fsPath.startsWith('/./')) { + if (fsPath.startsWith('/./') || fsPath.startsWith('\\.\\')) { relativePath = `.${fsPath.substr(1)}`; - } else if (fsPath.startsWith('//./')) { + } else if (fsPath.startsWith('//./') || fsPath.startsWith('\\\\.\\')) { relativePath = `.${fsPath.substr(2)}`; } diff --git a/src/vs/editor/contrib/peekView/browser/peekView.ts b/src/vs/editor/contrib/peekView/browser/peekView.ts index 5c0882b8db4..a0f2dfd914e 100644 --- a/src/vs/editor/contrib/peekView/browser/peekView.ts +++ b/src/vs/editor/contrib/peekView/browser/peekView.ts @@ -17,7 +17,7 @@ import 'vs/css!./media/peekViewWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IOptions, IStyles, ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; diff --git a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts index 20ba9ff079b..de4198e7446 100644 --- a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts +++ b/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/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 8b7bc9b51b8..b112dd1103c 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -6,7 +6,8 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { CancellationError, onUnexpectedError } from 'vs/base/common/errors'; +import { isMarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; @@ -37,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, RenameWidget, RenameWidgetResult } from './renameWidget'; class RenameSkeleton { @@ -137,7 +138,7 @@ class RenameController implements IEditorContribution { return editor.getContribution(RenameController.ID); } - private readonly _renameInputField: RenameInputField; + private readonly _renameWidget: RenameWidget; private readonly _disposableStore = new DisposableStore(); private _cts: CancellationTokenSource = new CancellationTokenSource(); @@ -152,7 +153,7 @@ class RenameController implements IEditorContribution { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { - this._renameInputField = this._disposableStore.add(this._instaService.createInstance(RenameInputField, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])); + this._renameWidget = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])); } dispose(): void { @@ -192,9 +193,15 @@ class RenameController implements IEditorContribution { this._progressService.showWhile(resolveLocationOperation, 250); loc = await resolveLocationOperation; trace('resolved rename location'); - } catch (e) { - trace('resolve rename location failed', JSON.stringify(e, null, '\t')); - MessageController.get(this.editor)?.showMessage(e || nls.localize('resolveRenameLocationFailed', "An unknown error occurred while resolving rename location"), position); + } catch (e: unknown) { + if (e instanceof CancellationError) { + trace('resolve rename location cancelled', JSON.stringify(e, null, '\t')); + } else { + trace('resolve rename location failed', e instanceof Error ? e : JSON.stringify(e, null, '\t')); + if (typeof e === 'string' || isMarkdownString(e)) { + MessageController.get(this.editor)?.showMessage(e || nls.localize('resolveRenameLocationFailed', "An unknown error occurred while resolving rename location"), position); + } + } return undefined; } finally { @@ -222,24 +229,19 @@ class RenameController implements IEditorContribution { const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled - 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`); - - const selection = this.editor.getSelection(); - let selectionStart = 0; - let selectionEnd = loc.text.length; - if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(loc.range, selection)) { - selectionStart = Math.max(0, selection.startColumn - loc.range.startColumn); - selectionEnd = Math.min(loc.range.endColumn, selection.endColumn) - loc.range.startColumn; - } + const requestRenameSuggestions = (cts: CancellationToken) => newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, cts)); trace('creating rename input field and awaiting its result'); const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); - const inputFieldResult = await this._renameInputField.getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, newSymbolNameProvidersResults, renameCandidatesCts); + const inputFieldResult = await this._renameWidget.getInput( + loc.range, + loc.text, + supportPreview, + requestRenameSuggestions, + cts2 + ); trace('received response from rename input field'); if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently @@ -317,22 +319,22 @@ class RenameController implements IEditorContribution { } acceptRenameInput(wantsPreview: boolean): void { - this._renameInputField.acceptInput(wantsPreview); + this._renameWidget.acceptInput(wantsPreview); } cancelRenameInput(): void { - this._renameInputField.cancelInput(true, 'cancelRenameInput command'); + this._renameWidget.cancelInput(true, 'cancelRenameInput command'); } focusNextRenameSuggestion(): void { - this._renameInputField.focusNextRenameSuggestion(); + this._renameWidget.focusNextRenameSuggestion(); } focusPreviousRenameSuggestion(): void { - this._renameInputField.focusPreviousRenameSuggestion(); + this._renameWidget.focusPreviousRenameSuggestion(); } - private _reportTelemetry(nRenameSuggestionProviders: number, languageId: string, inputFieldResult: boolean | RenameInputFieldResult) { + private _reportTelemetry(nRenameSuggestionProviders: number, languageId: string, inputFieldResult: boolean | RenameWidgetResult) { type RenameInvokedEvent = { kind: 'accepted' | 'cancelled'; @@ -340,10 +342,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; }; @@ -357,24 +361,27 @@ 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 }; }; - const value: RenameInvokedEvent = typeof inputFieldResult === 'boolean' - ? { - kind: 'cancelled', - languageId, - nRenameSuggestionProviders, - } - : { - kind: 'accepted', - languageId, - nRenameSuggestionProviders, - - source: inputFieldResult.source, - nRenameSuggestions: inputFieldResult.nRenameSuggestions, - wantsPreview: inputFieldResult.wantsPreview, - }; + const value: RenameInvokedEvent = + typeof inputFieldResult === 'boolean' + ? { + kind: 'cancelled', + languageId, + nRenameSuggestionProviders, + } + : { + kind: 'accepted', + languageId, + nRenameSuggestionProviders, + + source: inputFieldResult.stats.source.k, + nRenameSuggestions: inputFieldResult.stats.nRenameSuggestions, + timeBeforeFirstInputFieldEdit: inputFieldResult.stats.timeBeforeFirstInputFieldEdit, + wantsPreview: inputFieldResult.wantsPreview, + }; this._telemetryService.publicLog2('renameInvokedEvent', value); } @@ -459,7 +466,7 @@ registerEditorCommand(new RenameCommand({ kbOpts: { weight: KeybindingWeight.EditorContrib + 99, kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')), - primary: KeyMod.Shift + KeyCode.Enter + primary: KeyMod.CtrlCmd + KeyCode.Enter } })); @@ -514,12 +521,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/src/vs/editor/contrib/rename/browser/renameInputField.css b/src/vs/editor/contrib/rename/browser/renameWidget.css similarity index 100% rename from src/vs/editor/contrib/rename/browser/renameInputField.css rename to src/vs/editor/contrib/rename/browser/renameWidget.css diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts similarity index 52% rename from src/vs/editor/contrib/rename/browser/renameInputField.ts rename to src/vs/editor/contrib/rename/browser/renameWidget.ts index 3bb00ecdb05..bf475fde028 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -3,36 +3,41 @@ * 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 'vs/css!./renameWidget'; +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'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { NewSymbolName, NewSymbolNameTag, ProviderResult } from 'vs/editor/common/languages'; 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,92 @@ 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 RenameWidgetStats = { + nRenameSuggestions: number; + source: NewNameSource; + timeBeforeFirstInputFieldEdit: number | undefined; +}; + +export type RenameWidgetResult = { + /** + * The new name to be used + */ newName: string; wantsPreview?: boolean; - source: 'inputField' | 'renameSuggestion'; - nRenameSuggestions: number; + stats: RenameWidgetStats; +}; + +interface IRenameWidget { + /** + * @returns a `boolean` standing for `shouldFocusEditor`, if user didn't pick a new name, or a {@link RenameWidgetResult} + */ + getInput( + where: IRange, + currentName: string, + supportPreview: boolean, + requestRenameSuggestions: (cts: CancellationToken) => 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 RenameWidget implements IRenameWidget, 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 _renameCandidateProvidersCts: CancellationTokenSource | 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 +147,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 +180,34 @@ 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(); + if (this._renameCandidateProvidersCts?.token.isCancellationRequested === false) { + this._renameCandidateProvidersCts.cancel(); + } + this._renameCandidateListView?.clearFocus(); + }) + ); this._label = document.createElement('div'); this._label.className = 'rename-label'; @@ -131,7 +220,7 @@ export class RenameInputField implements IContentWidget { } private _updateStyles(theme: IColorTheme): void { - if (!this._input || !this._domNode) { + if (!this._domNode) { return; } @@ -142,26 +231,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, 'RenameWidget#_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 +266,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 +275,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 +310,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 +325,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 +346,136 @@ 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, + supportPreview: boolean, + requestRenameCandidates: (cts: CancellationToken) => ProviderResult[], + cts: CancellationTokenSource + ): Promise { + + const { start: selectionStart, end: selectionEnd } = this._getSelection(where, currentName); + + this._renameCandidateProvidersCts = new CancellationTokenSource(); + const candidates = requestRenameCandidates(this._renameCandidateProvidersCts.token); + this._updateRenameCandidates(candidates, currentName, cts.token); + + 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 + disposeOnDone.add(toDisposable(() => { + if (this._renameCandidateProvidersCts !== undefined) { + this._renameCandidateProvidersCts.dispose(true); + this._renameCandidateProvidersCts = undefined; + } + })); - this._updateRenameCandidates(candidates, value, cts.token); + const inputResult = new DeferredPromise(); - return new Promise(resolve => { + inputResult.p.finally(() => { + disposeOnDone.dispose(); + this._hide(); + }); - 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'; - } + this._currentCancelInput = (focusEditor) => { + this._trace('invoking _currentCancelInput'); + this._currentAcceptInput = undefined; + this._currentCancelInput = undefined; + this._renameCandidateListView?.clearCandidates(); + inputResult.complete(focusEditor); + return true; + }; - if (newName === value || newName.trim().length === 0 /* is just whitespace */) { - this.cancelInput(true, '_currentAcceptInput (because newName === value || newName.trim().length === 0)'); - return; - } + 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' }; + } + + 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._candidatesView.clearCandidates(); + this._currentAcceptInput = undefined; + this._currentCancelInput = undefined; + this._renameCandidateListView.clearCandidates(); - resolve({ - newName, - wantsPreview: supportPreview && wantsPreview, + 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; + } + + /** + * This allows selecting only part of the symbol name in the input field based on the selection in the editor + */ + private _getSelection(where: IRange, currentName: string): { start: number; end: number } { + assertType(this._editor.hasModel()); + + const selection = this._editor.getSelection(); + let start = 0; + let end = currentName.length; + + if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(where, selection)) { + start = Math.max(0, selection.startColumn - where.startColumn); + end = Math.min(where.endColumn, selection.endColumn) - where.startColumn; + } + + return { start, end }; } private _show(): void { @@ -356,11 +485,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 +517,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 +527,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'); @@ -416,21 +547,22 @@ export class RenameInputField implements IContentWidget { if (visibleRanges.length > 0) { firstLineInViewport = visibleRanges[0].startLineNumber; } else { - this._logService.warn('RenameInputField#_getTopForPosition: this should not happen - visibleRanges is empty'); + this._logService.warn('RenameWidget#_getTopForPosition: this should not happen - visibleRanges is empty'); firstLineInViewport = Math.max(1, this._position!.lineNumber - 5); // @ulugbekna: fallback to current line minus 5 } return this._editor.getTopForLineNumber(this._position!.lineNumber) - this._editor.getTopForLineNumber(firstLineInViewport); } - private _trace(...args: any[]) { - this._logService.trace('RenameInputField', ...args); + private _trace(...args: unknown[]) { + this._logService.trace('RenameWidget', ...args); } } -class CandidatesView { +class RenameCandidateListView { - private readonly _listWidget: List; + /** 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; @@ -439,7 +571,8 @@ class CandidatesView { 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(); @@ -450,62 +583,38 @@ class CandidatesView { 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; + this._listWidget = RenameCandidateListView._createListWidget(this._listContainer, this._candidateViewHeight, opts.fontInfo); - 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; - } - - 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() { @@ -562,27 +671,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]); } /** @@ -592,17 +697,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; } @@ -622,62 +737,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 { - // TODO@ulugbekna: accessibility + private _domNode: HTMLInputElement | undefined; + + 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/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts new file mode 100644 index 00000000000..f3296062d81 --- /dev/null +++ b/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/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts index c4a17ea7e76..dc9e9767d81 100644 --- a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts @@ -16,7 +16,7 @@ import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bro import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/browser/wordSelections'; import { createModelServices } from 'vs/editor/test/common/testTextModel'; -import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; +import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts index f1f50f9ced7..231052024c6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts +++ b/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/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 3388380f97c..705ef76489e 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/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/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 06fb2ce428f..bdcaafb4891 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -11,7 +11,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index fc41e265fc4..2eeb94d99b6 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -16,7 +16,7 @@ import { clamp } from 'vs/base/common/numbers'; import * as strings from 'vs/base/common/strings'; import 'vs/css!./media/suggest'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/browser/suggestWidgetStatus'; diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index 8b9790bce21..cae43742902 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -310,6 +310,11 @@ class WordHighlighter { this._onPositionChanged(e); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { + if (this.occurrencesHighlight === 'off') { + // Early exit if nothing needs to be done + return; + } + if (!this.workerRequest) { this._run(); } diff --git a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts index 6d78b460965..388d42021d2 100644 --- a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts +++ b/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/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts index 399d020170e..a06bf07200a 100644 --- a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts +++ b/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/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index 2ef43ea18a4..57d3dd3819b 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/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/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 050937165ff..72eb84fa998 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/editor/browser/coreCommands'; -import 'vs/editor/browser/widget/codeEditorWidget'; +import 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import 'vs/editor/browser/widget/diffEditor/diffEditor.contribution'; import 'vs/editor/contrib/anchorSelect/browser/anchorSelect'; import 'vs/editor/contrib/bracketMatching/browser/bracketMatching'; @@ -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/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 0c94d2824e9..709cb064db0 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/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/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 479bb75745c..c80bdf63686 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -7,7 +7,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { InternalEditorAction } from 'vs/editor/common/editorAction'; import { IModelChangedEvent } from 'vs/editor/common/editorCommon'; @@ -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'; /** @@ -55,7 +55,7 @@ export interface IActionDescriptor { */ label: string; /** - * Precondition rule. + * Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts). */ precondition?: string; /** diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 059a4928862..d1a70eba3d2 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -39,7 +39,7 @@ import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IMarker, IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { MultiDiffEditorWidget } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget'; +import { MultiDiffEditorWidget } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget'; /** * Create a new editor under `domElement`. @@ -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/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 8466fbf9f99..5ade938e7c2 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -809,6 +809,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { InlineEditTriggerKind: standaloneEnums.InlineEditTriggerKind, CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, NewSymbolNameTag: standaloneEnums.NewSymbolNameTag, + PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/editor/standalone/browser/standaloneLayoutService.ts b/src/vs/editor/standalone/browser/standaloneLayoutService.ts index cc946b22b97..8b55de680ba 100644 --- a/src/vs/editor/standalone/browser/standaloneLayoutService.ts +++ b/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/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 351f85537d8..c3d2f3c41c3 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -10,7 +10,7 @@ import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/editor/common/services/languageFeatureDebounce'; import 'vs/editor/common/services/semanticTokensStylingService'; import 'vs/editor/common/services/languageFeaturesService'; -import 'vs/editor/browser/services/hoverService'; +import 'vs/editor/browser/services/hoverService/hoverService'; import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; @@ -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/src/vs/editor/standalone/common/monarch/monarchCommon.ts b/src/vs/editor/standalone/common/monarch/monarchCommon.ts index 9c7febccbc8..7137ada8d6a 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCommon.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCommon.ts @@ -68,10 +68,10 @@ export function isIAction(what: FuzzyAction): what is IAction { } export interface IRule { - regex: RegExp; action: FuzzyAction; matchOnlyAtLineStart: boolean; name: string; + resolveRegex(state: string): RegExp; } export interface IAction { @@ -175,6 +175,26 @@ export function substituteMatches(lexer: ILexerMin, str: string, id: string, mat }); } +/** + * substituteMatchesRe is used on lexer regex rules and can substitutes predefined patterns: + * $Sn => n'th part of state + * + */ +export function substituteMatchesRe(lexer: ILexerMin, str: string, state: string): string { + const re = /\$[sS](\d\d?)/g; + let stateMatches: string[] | null = null; + return str.replace(re, function (full, s) { + if (stateMatches === null) { // split state on demand + stateMatches = state.split('.'); + stateMatches.unshift(state); + } + if (!empty(s) && s < stateMatches.length) { + return fixCase(lexer, stateMatches[s]); //$Sn + } + return ''; + }); +} + /** * Find the tokenizer rules for a specific state (i.e. next action) */ diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index 44d6bc3e922..e9f5f3934c4 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -85,7 +85,8 @@ function createKeywordMatcher(arr: string[], caseInsensitive: boolean = false): * @example /@attr/ will be replaced with the value of lexer[attr] * @example /@@text/ will not be replaced and will become /@text/. */ -function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { +function compileRegExp(lexer: monarchCommon.ILexerMin, str: string, handleSn: S): S extends true ? RegExp | DynamicRegExp : RegExp; +function compileRegExp(lexer: monarchCommon.ILexerMin, str: string, handleSn: true | false): RegExp | DynamicRegExp { // @@ must be interpreted as a literal @, so we replace all occurences of @@ with a placeholder character str = str.replace(/@@/g, `\x01`); @@ -116,6 +117,24 @@ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { str = str.replace(/\x01/g, '@'); const flags = (lexer.ignoreCase ? 'i' : '') + (lexer.unicode ? 'u' : ''); + + // handle $Sn + if (handleSn) { + const match = str.match(/\$[sS](\d\d?)/g); + if (match) { + let lastState: string | null = null; + let lastRegEx: RegExp | null = null; + return (state: string) => { + if (lastRegEx && lastState === state) { + return lastRegEx; + } + lastState = state; + lastRegEx = new RegExp(monarchCommon.substituteMatchesRe(lexer, str, state), flags); + return lastRegEx; + }; + } + } + return new RegExp(str, flags); } @@ -196,12 +215,12 @@ function createGuard(lexer: monarchCommon.ILexerMin, ruleName: string, tkey: str else if (op === '~' || op === '!~') { if (pat.indexOf('$') < 0) { // precompile regular expression - const re = compileRegExp(lexer, '^' + pat + '$'); + const re = compileRegExp(lexer, '^' + pat + '$', false); tester = function (s) { return (op === '~' ? re.test(s) : !re.test(s)); }; } else { tester = function (s, id, matches, state) { - const re = compileRegExp(lexer, '^' + monarchCommon.substituteMatches(lexer, pat, id, matches, state) + '$'); + const re = compileRegExp(lexer, '^' + monarchCommon.substituteMatches(lexer, pat, id, matches, state) + '$', false); return re.test(s); }; } @@ -355,11 +374,13 @@ function compileAction(lexer: monarchCommon.ILexerMin, ruleName: string, action: } } +type DynamicRegExp = (state: string) => RegExp; + /** * Helper class for creating matching rules */ class Rule implements monarchCommon.IRule { - public regex: RegExp = new RegExp(''); + private regex: RegExp | DynamicRegExp = new RegExp(''); public action: monarchCommon.FuzzyAction = { token: '' }; public matchOnlyAtLineStart: boolean = false; public name: string = ''; @@ -382,12 +403,20 @@ class Rule implements monarchCommon.IRule { this.matchOnlyAtLineStart = (sregex.length > 0 && sregex[0] === '^'); this.name = this.name + ': ' + sregex; - this.regex = compileRegExp(lexer, '^(?:' + (this.matchOnlyAtLineStart ? sregex.substr(1) : sregex) + ')'); + this.regex = compileRegExp(lexer, '^(?:' + (this.matchOnlyAtLineStart ? sregex.substr(1) : sregex) + ')', true); } public setAction(lexer: monarchCommon.ILexerMin, act: monarchCommon.IAction) { this.action = compileAction(lexer, this.name, act); } + + public resolveRegex(state: string): RegExp { + if (this.regex instanceof RegExp) { + return this.regex; + } else { + return this.regex(state); + } + } } /** diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index d0115c98dc1..d1d2c2c1f76 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -519,8 +519,8 @@ export class MonarchTokenizer extends Disposable implements languages.ITokenizat } hasEmbeddedPopRule = true; - let regex = rule.regex; - const regexSource = rule.regex.source; + let regex = rule.resolveRegex(state.stack.state); + const regexSource = regex.source; if (regexSource.substr(0, 4) === '^(?:' && regexSource.substr(regexSource.length - 1, 1) === ')') { const flags = (regex.ignoreCase ? 'i' : '') + (regex.unicode ? 'u' : ''); regex = new RegExp(regexSource.substr(4, regexSource.length - 5), flags); @@ -643,7 +643,7 @@ export class MonarchTokenizer extends Disposable implements languages.ITokenizat const restOfLine = line.substr(pos); for (const rule of rules) { if (pos === 0 || !rule.matchOnlyAtLineStart) { - matches = restOfLine.match(rule.regex); + matches = restOfLine.match(rule.resolveRegex(state)); if (matches) { matched = matches[0]; action = rule.action; diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index 881ea18973b..fd55718ca7d 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -346,4 +346,53 @@ suite('Monarch', () => { disposables.dispose(); }); + test('microsoft/monaco-editor#3128: allow state access within rules', () => { + const disposables = new DisposableStore(); + const configurationService = new StandaloneConfigurationService(); + const languageService = disposables.add(new LanguageService()); + + const tokenizer = disposables.add(createMonarchTokenizer(languageService, 'test', { + ignoreCase: false, + encoding: /u|u8|U|L/, + tokenizer: { + root: [ + // C++ 11 Raw String + [/@encoding?R\"(?:([^ ()\\\t]*))\(/, { token: 'string.raw.begin', next: '@raw.$1' }], + ], + + raw: [ + [/.*\)$S2\"/, 'string.raw', '@pop'], + [/.*/, 'string.raw'] + ], + }, + }, configurationService)); + + const lines = [ + `int main(){`, + ``, + ` auto s = R""""(`, + ` Hello World`, + ` )"""";`, + ``, + ` std::cout << "hello";`, + ``, + `}`, + ]; + + const actualTokens = getTokens(tokenizer, lines); + assert.deepStrictEqual(actualTokens, [ + [new Token(0, 'source.test', 'test')], + [], + [new Token(0, 'source.test', 'test'), new Token(10, 'string.raw.begin.test', 'test')], + [new Token(0, 'string.raw.test', 'test')], + [new Token(0, 'string.raw.test', 'test'), new Token(6, 'source.test', 'test')], + [], + [new Token(0, 'source.test', 'test')], + [], + [new Token(0, 'source.test', 'test')], + ]); + + disposables.dispose(); + }); + }); diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index 0de5fe05983..a769b21c4f9 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -14,7 +14,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { getEditOperation, testCommand } from 'vs/editor/test/browser/testCommand'; -import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; +import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { withEditorModel } from 'vs/editor/test/common/testTextModel'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index d440e01d723..5b4d0994a62 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/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/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index aef1e1cd2fa..4f644203ef4 100644 --- a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/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/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 854346ddfea..a86e95c1303 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -26,7 +26,6 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { OutgoingViewModelEventKind } from 'vs/editor/common/viewModelEventDispatcher'; import { ITestCodeEditor, TestCodeEditorInstantiationOptions, createCodeEditorServices, instantiateTestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { IRelaxedTextModelCreationOptions, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -4469,93 +4468,6 @@ suite('Editor Controller', () => { }); }); - test('issue #36090: JS: editor.autoIndent seems to be broken', () => { - const languageId = 'jsMode'; - - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - // ^(.*\*/)?\s*\}.*$ - decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/, - // ^.*\{[^}"']*$ - increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ - }, - onEnterRules: javascriptOnEnterRules - })); - - const model = createTextModel( - [ - 'class ItemCtrl {', - ' getPropertiesByItemId(id) {', - ' return this.fetchItem(id)', - ' .then(item => {', - ' return this.getPropertiesOfItem(item);', - ' });', - ' }', - '}', - ].join('\n'), - languageId - ); - - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel) => { - moveTo(editor, viewModel, 7, 6, false); - assertCursor(viewModel, new Selection(7, 6, 7, 6)); - - viewModel.type('\n', 'keyboard'); - assert.strictEqual(model.getValue(), - [ - 'class ItemCtrl {', - ' getPropertiesByItemId(id) {', - ' return this.fetchItem(id)', - ' .then(item => {', - ' return this.getPropertiesOfItem(item);', - ' });', - ' }', - ' ', - '}', - ].join('\n') - ); - assertCursor(viewModel, new Selection(8, 5, 8, 5)); - }); - }); - - test('issue #115304: OnEnter broken for TS', () => { - const languageId = 'jsMode'; - - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(languageConfigurationService.register(languageId, { - onEnterRules: javascriptOnEnterRules - })); - - const model = createTextModel( - [ - '/** */', - 'function f() {}', - ].join('\n'), - languageId - ); - - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel) => { - moveTo(editor, viewModel, 1, 4, false); - assertCursor(viewModel, new Selection(1, 4, 1, 4)); - - viewModel.type('\n', 'keyboard'); - assert.strictEqual(model.getValue(), - [ - '/**', - ' * ', - ' */', - 'function f() {}', - ].join('\n') - ); - assertCursor(viewModel, new Selection(2, 4, 2, 4)); - }); - }); test('issue #38261: TAB key results in bizarre indentation in C++ mode ', () => { const languageId = 'indentRulesMode'; diff --git a/src/vs/editor/browser/diff/testDiffProviderFactoryService.ts b/src/vs/editor/test/browser/diff/testDiffProviderFactoryService.ts similarity index 95% rename from src/vs/editor/browser/diff/testDiffProviderFactoryService.ts rename to src/vs/editor/test/browser/diff/testDiffProviderFactoryService.ts index 08ed249b8b9..275c4995988 100644 --- a/src/vs/editor/browser/diff/testDiffProviderFactoryService.ts +++ b/src/vs/editor/test/browser/diff/testDiffProviderFactoryService.ts @@ -18,7 +18,7 @@ export class TestDiffProviderFactoryService implements IDiffProviderFactoryServi } } -export class SyncDocumentDiffProvider implements IDocumentDiffProvider { +class SyncDocumentDiffProvider implements IDocumentDiffProvider { computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions, cancellationToken: CancellationToken): Promise { const result = linesDiffComputers.getDefault().computeDiff(original.getLinesContent(), modified.getLinesContent(), options); return Promise.resolve({ diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index b29e05c8053..c0a4b9d1cf3 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -9,7 +9,7 @@ import { EditorConfiguration } from 'vs/editor/browser/config/editorConfiguratio import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { View } from 'vs/editor/browser/view'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ILanguageService } from 'vs/editor/common/languages/language'; diff --git a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts new file mode 100644 index 00000000000..39aead0a848 --- /dev/null +++ b/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/src/vs/editor/test/common/core/random.ts b/src/vs/editor/test/common/core/random.ts new file mode 100644 index 00000000000..d48f4173f82 --- /dev/null +++ b/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/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts new file mode 100644 index 00000000000..f02e8a9bd50 --- /dev/null +++ b/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/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index 611ba267d5b..9155b32a9ca 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/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/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 262032dd9e4..007033dc790 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/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/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 16d237c00a5..91ec41810f3 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/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/src/vs/editor/test/common/modes/supports/indentationRules.ts b/src/vs/editor/test/common/modes/supports/indentationRules.ts new file mode 100644 index 00000000000..69dbd2b8fce --- /dev/null +++ b/src/vs/editor/test/common/modes/supports/indentationRules.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. + *--------------------------------------------------------------------------------------------*/ + +export const javascriptIndentationRules = { + decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/, + increaseIndentPattern: /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/, + // e.g. * ...| or */| or *-----*/| + unIndentedLinePattern: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, + indentNextLinePattern: /^((.*=>\s*)|((.*[^\w]+|\s*)(if|while|for)\s*\(.*\)\s*))$/, +}; + +export const rubyIndentationRules = { + decreaseIndentPattern: /^\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif)\b|(in|when)\s)/, + increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/, +}; + +export const phpIndentationRules = { + increaseIndentPattern: /({(?!.*}).*|\(|\[|((else(\s)?)?if|else|for(each)?|while|switch|case).*:)\s*((\/[/*].*|)?$|\?>)/, + decreaseIndentPattern: /^(.*\*\/)?\s*((\})|(\)+[;,])|(\]\)*[;,])|\b(else:)|\b((end(if|for(each)?|while|switch));))/, +}; + +export const goIndentationRules = { + decreaseIndentPattern: /^\s*(\bcase\b.*:|\bdefault\b:|}[)}]*[),]?|\)[,]?)$/, + increaseIndentPattern: /^.*(\bcase\b.*:|\bdefault\b:|(\b(func|if|else|switch|select|for|struct)\b.*)?{[^}"'`]*|\([^)"'`]*)$/, +}; diff --git a/src/vs/editor/test/common/modes/supports/javascriptIndentationRules.ts b/src/vs/editor/test/common/modes/supports/javascriptIndentationRules.ts deleted file mode 100644 index 12fb83c4925..00000000000 --- a/src/vs/editor/test/common/modes/supports/javascriptIndentationRules.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export const javascriptIndentationRules = { - decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/, - increaseIndentPattern: /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/, - // e.g. * ...| or */| or *-----*/| - unIndentedLinePattern: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/ -}; diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts deleted file mode 100644 index 5c8f580f228..00000000000 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.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 { IndentAction } from 'vs/editor/common/languages/languageConfiguration'; - -export const javascriptOnEnterRules = [ - { - // e.g. /** | */ - beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, - afterText: /^\s*\*\/$/, - action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' } - }, { - // e.g. /** ...| - beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, - action: { indentAction: IndentAction.None, appendText: ' * ' } - }, { - // e.g. * ...| - beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, - previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, - action: { indentAction: IndentAction.None, appendText: '* ' } - }, { - // e.g. */| - beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, - action: { indentAction: IndentAction.None, removeText: 1 } - }, - { - // e.g. *-----*/| - beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, - action: { indentAction: IndentAction.None, removeText: 1 } - } -]; diff --git a/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/src/vs/editor/test/common/modes/supports/onEnter.test.ts index 44b1af8d341..1daa14e1607 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { CharacterPair, IndentAction } from 'vs/editor/common/languages/languageConfiguration'; import { OnEnterSupport } from 'vs/editor/common/languages/supports/onEnter'; -import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; +import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/modes/supports/onEnterRules.ts b/src/vs/editor/test/common/modes/supports/onEnterRules.ts new file mode 100644 index 00000000000..a172d113c19 --- /dev/null +++ b/src/vs/editor/test/common/modes/supports/onEnterRules.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IndentAction } from 'vs/editor/common/languages/languageConfiguration'; + +export const javascriptOnEnterRules = [ + { + // e.g. /** | */ + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + afterText: /^\s*\*\/$/, + action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' } + }, { + // e.g. /** ...| + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + action: { indentAction: IndentAction.None, appendText: ' * ' } + }, { + // e.g. * ...| + beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, + previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, + action: { indentAction: IndentAction.None, appendText: '* ' } + }, { + // e.g. */| + beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, + action: { indentAction: IndentAction.None, removeText: 1 } + }, + { + // e.g. *-----*/| + beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, + action: { indentAction: IndentAction.None, removeText: 1 } + }, + { + beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/, + afterText: /^(?!\s*(\bcase\b|\bdefault\b))/, + action: { indentAction: IndentAction.Indent } + }, + { + previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/, + beforeText: /^\s+([^{i\s]|i(?!f\b))/, + action: { indentAction: IndentAction.Outdent } + }, + // Indent when pressing enter from inside () + { + beforeText: /^.*\([^\)]*$/, + afterText: /^\s*\).*$/, + action: { indentAction: IndentAction.IndentOutdent, appendText: '\t' } + }, + // Indent when pressing enter from inside {} + { + beforeText: /^.*\{[^\}]*$/, + afterText: /^\s*\}.*$/, + action: { indentAction: IndentAction.IndentOutdent, appendText: '\t' } + }, + // Indent when pressing enter from inside [] + { + beforeText: /^.*\[[^\]]*$/, + afterText: /^\s*\].*$/, + action: { indentAction: IndentAction.IndentOutdent, appendText: '\t' } + }, +]; + +export const phpOnEnterRules = [ + { + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + afterText: /^\s*\*\/$/, + action: { + indentAction: IndentAction.IndentOutdent, + appendText: ' * ', + } + }, + { + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + action: { + indentAction: IndentAction.None, + appendText: ' * ', + } + }, + { + beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/, + action: { + indentAction: IndentAction.None, + appendText: '* ', + } + }, + { + beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/, + action: { + indentAction: IndentAction.None, + removeText: 1, + } + }, + { + beforeText: /^(\t|(\ \ ))*\ \*[^/]*\*\/\s*$/, + action: { + indentAction: IndentAction.None, + removeText: 1, + } + }, + { + beforeText: /^\s+([^{i\s]|i(?!f\b))/, + previousLineText: /^\s*(((else ?)?if|for(each)?|while)\s*\(.*\)\s*|else\s*)$/, + action: { + indentAction: IndentAction.Outdent + } + }, +]; + +export const cppOnEnterRules = [ + { + previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/, + beforeText: /^\s+([^{i\s]|i(?!f\b))/, + action: { + indentAction: IndentAction.Outdent + } + } +]; + +/* +export enum IndentAction { + None = 0, + Indent = 1, + IndentOutdent = 2, + Outdent = 3 +} +*/ diff --git a/src/vs/editor/test/common/services/testEditorWorkerService.ts b/src/vs/editor/test/common/services/testEditorWorkerService.ts index e6693d821e9..e7d5154f9f6 100644 --- a/src/vs/editor/test/common/services/testEditorWorkerService.ts +++ b/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/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index 664ef33cf4f..995472ca78f 100644 --- a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts +++ b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts @@ -11,8 +11,11 @@ import { getLineRangeMapping } from 'vs/editor/common/diff/defaultLinesDiffCompu import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm'; import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('myers', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('1', () => { const s1 = new LinesSliceCharSequence(['hello world'], new OffsetRange(0, 1), true); const s2 = new LinesSliceCharSequence(['hallo welt'], new OffsetRange(0, 1), true); @@ -23,6 +26,8 @@ suite('myers', () => { }); suite('lineRangeMapping', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('Simple', () => { assert.deepStrictEqual( getLineRangeMapping( @@ -68,6 +73,8 @@ suite('lineRangeMapping', () => { }); suite('LinesSliceCharSequence', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + const sequence = new LinesSliceCharSequence( [ 'line1: foo', diff --git a/src/vs/editor/test/node/diffing/fixtures.test.ts b/src/vs/editor/test/node/diffing/fixtures.test.ts index e200d808c22..e944d133bef 100644 --- a/src/vs/editor/test/node/diffing/fixtures.test.ts +++ b/src/vs/editor/test/node/diffing/fixtures.test.ts @@ -12,8 +12,11 @@ import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { LegacyLinesDiffComputer } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { Range } from 'vs/editor/common/core/range'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('diffing fixtures', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + setup(() => { setUnexpectedErrorHandler(e => { throw e; diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-204948/1.txt b/src/vs/editor/test/node/diffing/fixtures/issue-204948/1.txt new file mode 100644 index 00000000000..42f5b92add2 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-204948/1.txt @@ -0,0 +1,9 @@ + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" + integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-204948/2.txt b/src/vs/editor/test/node/diffing/fixtures/issue-204948/2.txt new file mode 100644 index 00000000000..2b5867f0165 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-204948/2.txt @@ -0,0 +1,9 @@ + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-204948/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-204948/advanced.expected.diff.json new file mode 100644 index 00000000000..4dd454f4a45 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-204948/advanced.expected.diff.json @@ -0,0 +1,22 @@ +{ + "original": { + "content": " \"@babel/types\" \"^7.22.15\"\n\n\"@babel/traverse@^7.23.9\":\n version \"7.23.9\"\n resolved \"https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305\"\n integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==\n dependencies:\n \"@babel/code-frame\" \"^7.23.5\"\n \"@babel/generator\" \"^7.23.6\"", + "fileName": "./1.txt" + }, + "modified": { + "content": " \"@babel/types\" \"^7.22.15\"\n\n\"@babel/traverse@^7.23.9\":\n version \"7.23.9\"\n resolved \"https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305\"\n integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==\n dependencies:\n \"@babel/code-frame\" \"^7.23.5\"\n \"@babel/generator\" \"^7.23.6\"", + "fileName": "./2.txt" + }, + "diffs": [ + { + "originalRange": "[6,7)", + "modifiedRange": "[6,7)", + "innerChanges": [ + { + "originalRange": "[6,20 -> 7,1]", + "modifiedRange": "[6,20 -> 7,1]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-204948/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-204948/legacy.expected.diff.json new file mode 100644 index 00000000000..97e5b8d4e12 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-204948/legacy.expected.diff.json @@ -0,0 +1,22 @@ +{ + "original": { + "content": " \"@babel/types\" \"^7.22.15\"\n\n\"@babel/traverse@^7.23.9\":\n version \"7.23.9\"\n resolved \"https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305\"\n integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==\n dependencies:\n \"@babel/code-frame\" \"^7.23.5\"\n \"@babel/generator\" \"^7.23.6\"", + "fileName": "./1.txt" + }, + "modified": { + "content": " \"@babel/types\" \"^7.22.15\"\n\n\"@babel/traverse@^7.23.9\":\n version \"7.23.9\"\n resolved \"https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305\"\n integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==\n dependencies:\n \"@babel/code-frame\" \"^7.23.5\"\n \"@babel/generator\" \"^7.23.6\"", + "fileName": "./2.txt" + }, + "diffs": [ + { + "originalRange": "[6,7)", + "modifiedRange": "[6,7)", + "innerChanges": [ + { + "originalRange": "[6,20 -> 6,105]", + "modifiedRange": "[6,20 -> 6,105]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 939685005ab..514a95f9d88 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1251,7 +1251,7 @@ declare namespace monaco.editor { */ label: string; /** - * Precondition rule. + * Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts). */ precondition?: string; /** @@ -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 `~!@#$%^&*()-=+[{]}\\|;:\'",.<>/? @@ -3828,6 +3851,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. @@ -4289,6 +4316,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; } /** @@ -4938,27 +4977,28 @@ 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, - quickSuggestionsMinimumLength = 149, - tabSuggest = 150 + 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, + quickSuggestionsMinimumLength = 150, + tabSuggest = 151 } export const EditorOptions: { @@ -5098,6 +5138,7 @@ declare namespace monaco.editor { useShadowDOM: IEditorOption; useTabStops: IEditorOption; wordBreak: IEditorOption; + wordSegmenterLocales: IEditorOption; wordSeparators: IEditorOption; wordWrap: IEditorOption; wordWrapBreakAfterCharacters: IEditorOption; @@ -5625,6 +5666,7 @@ declare namespace monaco.editor { export interface IPasteEvent { readonly range: Range; readonly languageId: string | null; + readonly clipboardEvent?: ClipboardEvent; } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions, IEditorConstructionOptions { @@ -6970,6 +7012,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. */ @@ -7116,7 +7174,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. */ @@ -7864,7 +7922,7 @@ declare namespace monaco.languages { body: string; range: IRange | undefined; uri: Uri; - owner: string; + uniqueOwner: string; isReply: boolean; } diff --git a/src/vs/nls.mock.ts b/src/vs/nls.mock.ts index d9ee1ecd2c6..5323c6c6340 100644 --- a/src/vs/nls.mock.ts +++ b/src/vs/nls.mock.ts @@ -8,7 +8,7 @@ export interface ILocalizeInfo { comment: string[]; } -interface ILocalizedString { +export interface ILocalizedString { original: string; value: string; } diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index d2689b002d7..de4044b74f4 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/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/src/vs/platform/accessibilitySignal/browser/media/terminalBell.mp3 b/src/vs/platform/accessibilitySignal/browser/media/terminalBell.mp3 index f00aa6de2bb..7c6b7fe8323 100644 Binary files a/src/vs/platform/accessibilitySignal/browser/media/terminalBell.mp3 and b/src/vs/platform/accessibilitySignal/browser/media/terminalBell.mp3 differ diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 new file mode 100644 index 00000000000..488754fdd58 Binary files /dev/null and b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 differ diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 new file mode 100644 index 00000000000..0532cf6b15a Binary files /dev/null and b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStopped.mp3 differ diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 4b0b93f695d..8eeeee2df29 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -130,9 +130,9 @@ class ActionItemRenderer implements IListRenderer, IAction data.container.title = element.label; } else if (actionTitle && previewTitle) { if (this._supportsPreview && element.canPreview) { - data.container.title = localize({ key: 'label-preview', comment: ['placeholders are keybindings, e.g "F2 to apply, Shift+F2 to preview"'] }, "{0} to apply, {1} to preview", actionTitle, previewTitle); + data.container.title = localize({ key: 'label-preview', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", actionTitle, previewTitle); } else { - data.container.title = localize({ key: 'label', comment: ['placeholder is a keybinding, e.g "F2 to apply"'] }, "{0} to apply", actionTitle); + data.container.title = localize({ key: 'label', comment: ['placeholder is a keybinding, e.g "F2 to Apply"'] }, "{0} to Apply", actionTitle); } } else { data.container.title = ''; @@ -140,7 +140,7 @@ class ActionItemRenderer implements IListRenderer, IAction } disposeTemplate(_templateData: IActionMenuTemplateData): void { - // noop + _templateData.keybinding.dispose(); } } diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index 21d4c4c5fc6..79cbe6faa37 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; +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'; @@ -29,6 +31,7 @@ export interface IWorkbenchButtonBarOptions { export class WorkbenchButtonBar extends ButtonBar { protected readonly _store = new DisposableStore(); + protected readonly _updateStore = new DisposableStore(); private readonly _actionRunner: IActionRunner; private readonly _onDidChange = new Emitter(); @@ -57,6 +60,7 @@ export class WorkbenchButtonBar extends ButtonBar { override dispose() { this._onDidChange.dispose(); + this._updateStore.dispose(); this._store.dispose(); super.dispose(); } @@ -65,8 +69,12 @@ export class WorkbenchButtonBar extends ButtonBar { const conifgProvider: IButtonConfigProvider = this._options?.buttonConfigProvider ?? (() => ({ showLabel: true })); + this._updateStore.clear(); this.clear(); + // Support instamt hover between buttons + const hoverDelegate = this._updateStore.add(createInstantHoverDelegate()); + for (let i = 0; i < actions.length; i++) { const secondary = i > 0; @@ -107,15 +115,16 @@ export class WorkbenchButtonBar extends ButtonBar { } } const kb = this._keybindingService.lookupKeybinding(action.id); + let tooltip: string; if (kb) { - btn.element.title = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel()); + tooltip = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel()); } else { - btn.element.title = action.label; - + tooltip = action.label; } - btn.onDidClick(async () => { + this._updateStore.add(setupCustomHover(hoverDelegate, btn.element, tooltip)); + this._updateStore.add(btn.onDidClick(async () => { this._actionRunner.run(action); - }); + })); } this._onDidChange.fire(this); } diff --git a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index cfcc0e2c73b..13ddcb79d8a 100644 --- a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts +++ b/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; @@ -134,7 +134,9 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { menuAsChild: true, classNames: ['codicon', dropdownIcon || 'codicon-chevron-down'], - hoverDelegate: this._options?.hoverDelegate + actionRunner: this._options?.actionRunner, + hoverDelegate: this._options?.hoverDelegate, + keybindingProvider: this._options?.getKeyBinding }); if (this._dropdownContainer) { this._dropdown.render(this._dropdownContainer); diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index c1edd287bea..da596a8fc6c 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/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/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 4791f56fcea..35f8d5355f6 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -167,6 +167,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'); @@ -187,6 +188,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'); @@ -199,6 +202,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'); @@ -219,9 +223,13 @@ export class MenuId { static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); static readonly ChatExecute = new MenuId('ChatExecute'); + static readonly ChatExecuteSecondary = new MenuId('ChatExecuteSecondary'); 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/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index 10158c8d75c..87c58811d8d 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/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/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index f47285746fe..beaf0b894e6 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/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/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 24e2e4c5506..159bea6fc8e 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/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/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index cc157af7ab3..18d4fcd795a 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -140,4 +140,5 @@ export interface NativeParsedArgs { 'log-net-log'?: string; 'vmodule'?: string; 'disable-dev-shm-usage'?: boolean; + 'ozone-platform'?: string; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 0d3a9795ffb..327c18c18ad 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -204,6 +204,7 @@ export const OPTIONS: OptionDescriptions> = { '_urls': { type: 'string[]' }, 'disable-dev-shm-usage': { type: 'boolean' }, 'profile-temp': { type: 'boolean' }, + 'ozone-platform': { type: 'string' }, _: { type: 'string[]' } // main arguments }; diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index ced410cb52e..5360015fbfb 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -16,7 +16,8 @@ import * as nls from 'vs/nls'; import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, - InstallOptions, 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()}`)) { @@ -338,6 +340,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } catch (error) { // rollback installed extensions if (successResults.length) { + this.logService.info('Rollback: Uninstalling installed extensions', getErrorMessage(error)); await Promise.allSettled(successResults.map(async ({ local, profileLocation }) => { try { await this.createUninstallExtensionTask(local, { versionOnly: true, profileLocation }).run(); @@ -405,12 +408,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 +445,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 +465,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 +476,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 +488,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 +511,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 +718,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/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 47b23ede32b..e2fa015a4ce 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -295,6 +295,7 @@ type GalleryServiceAdditionalQueryEvent = { }; interface IExtensionCriteria { + readonly productVersion: IProductVersion; readonly targetPlatform: TargetPlatform; readonly compatible: boolean; readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[]; @@ -662,14 +663,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi query = query.withSource(options.source); } - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); if (options.source) { extensions.forEach((e, index) => setTelemetry(e, index, options.source)); } return extensions; } - async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) { return null; } @@ -680,11 +681,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi .withFlags(Flags.IncludeVersions) .withPage(1, 1) .withFilter(FilterType.ExtensionId, extension.identifier.uuid); - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease }, CancellationToken.None); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease, productVersion }, CancellationToken.None); return extensions[0] || null; } - async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { return false; } @@ -707,10 +708,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; } @@ -721,8 +722,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) { @@ -789,7 +790,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 }; }; @@ -918,7 +919,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) { @@ -1050,7 +1051,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); } @@ -1062,13 +1063,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 || ''; } @@ -1077,27 +1078,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; } @@ -1106,7 +1107,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 || ''; } @@ -1137,7 +1138,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 */ } @@ -1155,7 +1156,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 || {}) }; @@ -1181,24 +1182,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/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 756acf86b45..9dae82eba07 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -20,6 +20,11 @@ export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export interface IProductVersion { + readonly version: string; + readonly date?: string; +} + export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { case TargetPlatform.WIN32_X64: return 'Windows 64 bit'; @@ -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/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index d4f15349aad..6c3e289db7d 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, 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/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 4fcf403d999..fddaf329af0 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -83,7 +83,7 @@ export interface IExtensionsProfileScannerService { readonly onDidRemoveExtensions: Event; scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise; - addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; + addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise; updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise; } @@ -120,18 +120,22 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable return this.withProfileExtensions(profileLocation, undefined, options); } - async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise { + async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; const extensionsToAdd: IScannedProfileExtension[] = []; try { await this.withProfileExtensions(profileLocation, existingExtensions => { const result: IScannedProfileExtension[] = []; - for (const existing of existingExtensions) { - if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { - // Remove the existing extension with different version - extensionsToRemove.push(existing); - } else { - result.push(existing); + if (keepExistingVersions) { + result.push(...existingExtensions); + } else { + for (const existing of existingExtensions) { + if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { + // Remove the existing extension with different version + extensionsToRemove.push(existing); + } else { + result.push(existing); + } } } for (const [extension, metadata] of extensions) { diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 3d338d1945e..5d3fc451349 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -22,7 +22,7 @@ import { isEmptyObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; @@ -112,6 +112,7 @@ export type ScanOptions = { readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; + readonly productVersion?: IProductVersion; }; export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); @@ -130,6 +131,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; @@ -199,7 +201,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 { @@ -221,7 +223,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 @@ -237,7 +239,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; @@ -249,11 +251,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(); @@ -396,7 +407,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); @@ -426,7 +437,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); } @@ -440,7 +451,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; @@ -455,8 +466,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem type, excludeObsolete, validate, - this.productService.version, - this.productService.date, + productVersion.version, + productVersion.date, this.productService.commit, // --- Start Positron --- this.productService.positronVersion, @@ -480,6 +491,13 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return undefined; } + private getProductVersion(): IProductVersion { + return { + version: this.productService.version, + date: this.productService.date, + }; + } + } export class ExtensionScannerInput { diff --git a/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts index 7864019ad83..98f5a2194f9 100644 --- a/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts +++ b/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/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 217d7720d15..56e32e7a269 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -28,7 +28,8 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, - Metadata, 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 { @@ -287,7 +288,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const key = ExtensionKey.create(extension).toString(); let installExtensionTask = this.installGalleryExtensionsTasks.get(key); if (!installExtensionTask) { - this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService)); + this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService, this.telemetryService)); installExtensionTask.waitUntilTaskIsFinished().finally(() => this.installGalleryExtensionsTasks.delete(key)); } return installExtensionTask; @@ -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 +909,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) { @@ -917,6 +922,42 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { } } + try { + return await this.downloadAndInstallExtension(metadata, token); + } catch (error) { + if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.CorruptZip || error.code === ExtensionManagementErrorCode.IncompleteZip)) { + this.logService.info(`Downloaded VSIX is invalid. Trying to download and install again...`, this.gallery.identifier.id); + type RetryInstallingInvalidVSIXClassification = { + owner: 'sandy081'; + comment: 'Event reporting the retry of installing an invalid VSIX'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; + succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; + }; + type RetryInstallingInvalidVSIXEvent = { + extensionId: string; + succeeded: boolean; + }; + try { + const result = await this.downloadAndInstallExtension(metadata, token); + this.telemetryService.publicLog2('extensiongallery:install:retry', { + extensionId: this.gallery.identifier.id, + succeeded: true + }); + return result; + } catch (error) { + this.telemetryService.publicLog2('extensiongallery:install:retry', { + extensionId: this.gallery.identifier.id, + succeeded: false + }); + throw error; + } + } else { + throw error; + } + } + } + + private async downloadAndInstallExtension(metadata: Metadata, token: CancellationToken): Promise<[ILocalExtension, Metadata]> { const { location, verificationStatus } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); try { this._verificationStatus = verificationStatus; @@ -969,7 +1010,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 +1018,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/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index 66d1ffc28e2..05d218b50b5 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/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/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index 1767e8931cb..1b27c8504d8 100644 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -102,7 +102,7 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { engines: { vscode: '*' }, }, extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource }, + { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, extensionDownloader, new TestExtensionsScanner(), uriIdentityService, @@ -110,6 +110,7 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { extensionsScannerService, extensionsProfileScannerService, logService, + NullTelemetryService ); } diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index 118cbf8d5ec..07639a7e7b6 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/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/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts b/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts index d148e475b32..343051cce38 100644 --- a/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts +++ b/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts @@ -16,10 +16,10 @@ import { getServiceMachineId } from 'vs/platform/externalServices/common/service import { IStorageService } from 'vs/platform/storage/common/storage'; import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; -import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; +import { RemoteAuthorities } from 'vs/base/common/network'; import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; -const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource'; +const WEB_EXTENSION_RESOURCE_END_POINT_SEGMENT = '/web-extension-resource/'; export const IExtensionResourceLoaderService = createDecorator('extensionResourceLoaderService'); @@ -66,7 +66,6 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi readonly _serviceBrand: undefined; - private readonly _webExtensionResourceEndPoint: string; private readonly _extensionGalleryResourceUrlTemplate: string | undefined; private readonly _extensionGalleryAuthority: string | undefined; @@ -77,7 +76,6 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi private readonly _environmentService: IEnvironmentService, private readonly _configurationService: IConfigurationService, ) { - this._webExtensionResourceEndPoint = `${getRemoteServerRootPath(_productService)}/${WEB_EXTENSION_RESOURCE_END_POINT}/`; if (_productService.extensionsGallery) { this._extensionGalleryResourceUrlTemplate = _productService.extensionsGallery.resourceUrlTemplate; this._extensionGalleryAuthority = this._extensionGalleryResourceUrlTemplate ? this._getExtensionGalleryAuthority(URI.parse(this._extensionGalleryResourceUrlTemplate)) : undefined; @@ -143,7 +141,9 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi } protected _isWebExtensionResourceEndPoint(uri: URI): boolean { - return uri.path.startsWith(this._webExtensionResourceEndPoint); + const uriPath = uri.path, serverRootPath = RemoteAuthorities.getServerRootPath(); + // test if the path starts with the server root path followed by the web extension resource end point segment + return uriPath.startsWith(serverRootPath) && uriPath.startsWith(WEB_EXTENSION_RESOURCE_END_POINT_SEGMENT, serverRootPath.length); } } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 86fa8c1c363..bfd6759fba9 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -255,6 +255,8 @@ export const EXTENSION_CATEGORIES = [ 'Testing', 'Themes', 'Visualization', + 'AI', + 'Chat', 'Other', ]; diff --git a/src/vs/platform/externalTerminal/node/externalTerminalService.ts b/src/vs/platform/externalTerminal/node/externalTerminalService.ts index a8df823266a..5086c95a802 100644 --- a/src/vs/platform/externalTerminal/node/externalTerminalService.ts +++ b/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/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 0bc285f082e..e8bcce418a8 100644 --- a/src/vs/platform/files/common/files.ts +++ b/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/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index ae97f833078..3aeba5d80ad 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/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/src/vs/platform/files/electron-main/diskFileSystemProviderServer.ts b/src/vs/platform/files/electron-main/diskFileSystemProviderServer.ts index fdc7e6a9117..64cbede2fd1 100644 --- a/src/vs/platform/files/electron-main/diskFileSystemProviderServer.ts +++ b/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/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts new file mode 100644 index 00000000000..9b9c35ae6c3 --- /dev/null +++ b/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/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts index 11eb6b8a109..2a662eb7e05 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts +++ b/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/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index dac55a138c5..197c975a465 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/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/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 8f0f7d6a87f..ba9edc88f0f 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/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/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index d1b978043ed..3ba2370fbe2 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/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/src/vs/platform/files/node/watcher/watcher.ts b/src/vs/platform/files/node/watcher/watcher.ts index e266239cb65..d0b563e540c 100644 --- a/src/vs/platform/files/node/watcher/watcher.ts +++ b/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/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 74dbb343c97..177756f62eb 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/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/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 42e8b4ad730..18a1a538433 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/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/src/vs/platform/hover/browser/hover.ts b/src/vs/platform/hover/browser/hover.ts index c9edff5d2a3..9213e1ea02f 100644 --- a/src/vs/platform/hover/browser/hover.ts +++ b/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,17 +280,34 @@ 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); } - setOptions(options: Partial | ((options: IHoverDelegateOptions, focus?: boolean) => Partial)): void { - this.overrideOptions = options; + 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'); + } + this.timeLimit = timeLimit; } onDidHideHover(): void { diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index d1c4e29bb63..df2a1e27599 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/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/src/vs/platform/layout/browser/layoutService.ts b/src/vs/platform/layout/browser/layoutService.ts index 6fad206c73e..65de0312451 100644 --- a/src/vs/platform/layout/browser/layoutService.ts +++ b/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/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index e2d69c0fe30..28fc419bbaf 100644 --- a/src/vs/platform/log/common/log.ts +++ b/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/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 0f2ef2566fc..5294be4aefa 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/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/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 2ab5bcecbfa..f11b38cb19d 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/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/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 2b455fa8dc6..147c49f2da7 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -12,6 +12,9 @@ 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'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; export interface ILinkDescriptor { readonly label: string | HTMLElement; @@ -22,12 +25,16 @@ export interface ILinkDescriptor { export interface ILinkOptions { readonly opener?: (href: string) => void; + readonly hoverDelegate?: IHoverDelegate; readonly textLinkForeground?: string; } export class Link extends Disposable { private el: HTMLAnchorElement; + private hover?: ICustomHover; + private hoverDelegate: IHoverDelegate; + private _enabled: boolean = true; get enabled(): boolean { @@ -68,9 +75,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 +91,11 @@ 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.hoverDelegate = options.hoverDelegate ?? getDefaultHoverDelegate('mouse'); + this.setTooltip(_link.title); + this.el.setAttribute('role', 'button'); const onClickEmitter = this._register(new DomEmitter(this.el, 'click')); @@ -117,4 +124,14 @@ export class Link extends Disposable { this.enabled = true; } + + private setTooltip(title: string | undefined): void { + if (this.hoverDelegate.showNativeHover) { + this.el.title = title ?? ''; + } else if (!this.hover && title) { + this.hover = this._register(setupCustomHover(this.hoverDelegate, this.el, title)); + } else if (this.hover) { + this.hover.update(title); + } + } } diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 87a211bcfbf..7aabb2705dd 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/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/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 8756dc7c60a..3afa06e5888 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/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 { @@ -60,7 +59,7 @@ .quick-input-header { display: flex; - padding: 8px 6px 6px 6px; + padding: 8px 6px 2px 6px; } .quick-input-widget.hidden-input .quick-input-header { @@ -264,8 +263,16 @@ overflow: hidden; } -.quick-input-list .monaco-highlighted-label .highlight { +/* preserve list-like styling instead of tree-like styling */ +.quick-input-list .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { font-weight: bold; + background-color: unset; + color: var(--vscode-list-highlightForeground) !important; +} + +/* preserve list-like styling instead of tree-like styling */ +.quick-input-list .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { + color: var(--vscode-list-focusHighlightForeground) !important; } .quick-input-list .quick-input-list-entry .quick-input-list-separator { @@ -301,7 +308,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; } @@ -314,8 +323,32 @@ background: none; } -/* Quick input separators as full-row item */ .quick-input-list .quick-input-list-separator-as-item { - font-weight: 600; + padding: 4px 6px; font-size: 12px; } + +/* Quick input separators as full-row item */ +.quick-input-list .quick-input-list-separator-as-item .label-name { + font-weight: 600; +} + +.quick-input-list .quick-input-list-separator-as-item .label-description { + /* Override default description opacity so we don't have a contrast ratio issue. */ + opacity: 1 !important; +} + +/* 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/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index acde1e461d1..86160c9e26a 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -6,7 +6,7 @@ import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { isFunction } from 'vs/base/common/types'; @@ -59,6 +59,22 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; } +export interface IPickerQuickAccessSeparator extends IQuickPickSeparator { + /** + * A method that will be executed when a button of the pick item was + * clicked on. + * + * @param buttonIndex index of the button of the item that + * was clicked. + * + * @param the state of modifier keys when the button was triggered. + * + * @returns a value that indicates what should happen after the trigger + * which can be a `Promise` for long running operations. + */ + trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; +} + export interface IPickerQuickAccessProviderOptions { /** @@ -320,47 +336,52 @@ export abstract class PickerQuickAccessProvider { - if (typeof item.trigger === 'function') { - const buttonIndex = item.buttons?.indexOf(button) ?? -1; - if (buttonIndex >= 0) { - const result = item.trigger(buttonIndex, picker.keyMods); - const action = (typeof result === 'number') ? result : await result; - - if (token.isCancellationRequested) { - return; - } + const buttonTrigger = async (button: IQuickInputButton, item: T | IPickerQuickAccessSeparator) => { + if (typeof item.trigger !== 'function') { + return; + } - switch (action) { - case TriggerAction.NO_ACTION: - break; - case TriggerAction.CLOSE_PICKER: - picker.hide(); - break; - case TriggerAction.REFRESH_PICKER: - updatePickerItems(); - break; - case TriggerAction.REMOVE_ITEM: { - const index = picker.items.indexOf(item); - if (index !== -1) { - const items = picker.items.slice(); - const removed = items.splice(index, 1); - const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); - const keepScrollPositionBefore = picker.keepScrollPosition; - picker.keepScrollPosition = true; - picker.items = items; - if (activeItems) { - picker.activeItems = activeItems; - } - picker.keepScrollPosition = keepScrollPositionBefore; + const buttonIndex = item.buttons?.indexOf(button) ?? -1; + if (buttonIndex >= 0) { + const result = item.trigger(buttonIndex, picker.keyMods); + const action = (typeof result === 'number') ? result : await result; + + if (token.isCancellationRequested) { + return; + } + + switch (action) { + case TriggerAction.NO_ACTION: + break; + case TriggerAction.CLOSE_PICKER: + picker.hide(); + break; + case TriggerAction.REFRESH_PICKER: + updatePickerItems(); + break; + case TriggerAction.REMOVE_ITEM: { + const index = picker.items.indexOf(item); + if (index !== -1) { + const items = picker.items.slice(); + const removed = items.splice(index, 1); + const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); + const keepScrollPositionBefore = picker.keepScrollPosition; + picker.keepScrollPosition = true; + picker.items = items; + if (activeItems) { + picker.activeItems = activeItems; } - break; + picker.keepScrollPosition = keepScrollPositionBefore; } + break; } } } - })); + }; + + // Trigger the pick with button index if button triggered + disposables.add(picker.onDidTriggerItemButton(({ button, item }) => buttonTrigger(button, item))); + disposables.add(picker.onDidTriggerSeparatorButton(({ button, separator }) => buttonTrigger(button, separator))); return disposables; } diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index cb35e451aa1..88169c32ed5 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -92,6 +92,10 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } } + // Store the existing selection if there was one. + const visibleSelection = visibleQuickAccess?.picker?.valueSelection; + const visibleValue = visibleQuickAccess?.picker?.value; + // Create a picker for the provider to use with the initial value // and adjust the filtering to exclude the prefix from filtering const disposables = new DisposableStore(); @@ -148,6 +152,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // on the onDidHide event. picker.show(); + // If the previous picker had a selection and the value is unchanged, we should set that in the new picker. + if (visibleSelection && visibleValue === value) { + picker.valueSelection = visibleSelection; + } + // Pick mode: return with promise if (pick) { return pickPromise?.p; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 96dfeee7ea8..0cce0ef4228 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/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; @@ -725,7 +723,15 @@ export class QuickPick extends QuickInput implements I return this.ui.keyMods; } - set valueSelection(valueSelection: Readonly<[number, number]>) { + get valueSelection() { + const selection = this.ui.inputBox.getSelection(); + if (!selection) { + return undefined; + } + return [selection.start, selection.end]; + } + + set valueSelection(valueSelection: Readonly<[number, number]> | undefined) { this._valueSelection = valueSelection; this.valueSelectionUpdated = true; this.update(); @@ -820,20 +826,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 +1077,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 +1085,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: @@ -1154,7 +1175,15 @@ export class InputBox extends QuickInput implements IInputBox { this.update(); } - set valueSelection(valueSelection: Readonly<[number, number]>) { + get valueSelection() { + const selection = this.ui.inputBox.getSelection(); + if (!selection) { + return undefined; + } + return [selection.start, selection.end]; + } + + set valueSelection(valueSelection: Readonly<[number, number]> | undefined) { this._valueSelection = valueSelection; this.valueSelectionUpdated = true; this.update(); diff --git a/src/vs/platform/quickinput/browser/quickInputBox.ts b/src/vs/platform/quickinput/browser/quickInputBox.ts index b3c6694387c..9ee1440a91a 100644 --- a/src/vs/platform/quickinput/browser/quickInputBox.ts +++ b/src/vs/platform/quickinput/browser/quickInputBox.ts @@ -59,6 +59,10 @@ export class QuickInputBox extends Disposable { this.findInput.inputBox.select(range); } + getSelection(): IRange | null { + return this.findInput.inputBox.getSelection(); + } + isSelectionAtEnd(): boolean { return this.findInput.inputBox.isSelectionAtEnd(); } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index d5a23ae66e8..e64440ba2a7 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/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/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts deleted file mode 100644 index de219a800bb..00000000000 --- a/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/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index 2974eed8077..e23c1158e68 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/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/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts new file mode 100644 index 00000000000..2c13515616a --- /dev/null +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -0,0 +1,1700 @@ +/*--------------------------------------------------------------------------------------------- + * 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, ITreeEvent, 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; + 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; + + constructor( + readonly index: number, + readonly hasCheckbox: boolean, + mainItem: QuickPickItem + ) { + 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 + }; + }); + this._saneDescription = mainItem.description; + this._saneTooltip = mainItem.tooltip; + } + + // #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 = false; + get hidden() { + return this._hidden; + } + set hidden(value: boolean) { + this._hidden = value; + } + + private _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; + } + + private _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 { + readonly onChecked: Event; + + constructor( + index: number, + hasCheckbox: boolean, + readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void, + private _onChecked: Emitter<{ element: IQuickPickElement; checked: boolean }>, + readonly item: IQuickPickItem, + private _separator: IQuickPickSeparator | undefined, + ) { + super(index, hasCheckbox, item); + + this.onChecked = hasCheckbox + ? Event.map(Event.filter<{ element: IQuickPickElement; checked: boolean }>(this._onChecked.event, e => e.element === this), e => e.checked) + : Event.None; + + this._saneDetail = item.detail; + 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; + } + + private _checked = false; + get checked() { + return this._checked; + } + set checked(value: boolean) { + if (value !== this._checked) { + this._checked = value; + this._onChecked.fire({ element: this, checked: value }); + } + } + + get checkboxDisabled() { + return !!this.item.disabled; + } +} + +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, + readonly separator: IQuickPickSeparator, + ) { + super(index, false, separator); + } +} + +class QuickInputItemDelegate implements IListVirtualDelegate { + getHeight(element: IQuickPickElement): number { + + if (element instanceof QuickPickSeparatorElement) { + return 30; + } + 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 || !(element instanceof QuickPickItemElement)) { + 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'; + + // 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; + } + + override renderTemplate(container: HTMLElement): IQuickInputItemTemplateData { + const data = super.renderTemplate(container); + + data.toDisposeTemplate.add(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { + (data.element as QuickPickItemElement).checked = data.checkbox.checked; + })); + + return data; + } + + 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)); + data.checkbox.disabled = element.checkboxDisabled; + + const { labelHighlights, descriptionHighlights, detailHighlights } = element; + + // Icon + 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; + + // Icon + data.icon.style.backgroundImage = ''; + data.icon.className = ''; + + // 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 _onTriggerEmptySelectionOrFocus = new Emitter>(); + + 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; + // This is used to prevent setting the checked state of a single element from firing the checked events + // so that we can batch them together. This can probably be improved by handling events differently, + // but this works for now. An observable would probably be ideal for this. + private _shouldFireCheckedEvents = true; + + 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( + Event.any(this._tree.onDidChangeFocus, this._onTriggerEmptySelectionOrFocus.event), + e => e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item) + ); + } + + @memoize + get onDidChangeSelection() { + return Event.map( + Event.any(this._tree.onDidChangeSelection, this._onTriggerEmptySelectionOrFocus.event), + 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) { + try { + this._shouldFireCheckedEvents = false; + this._itemElements.forEach(element => { + if (!element.hidden && !element.checkboxDisabled) { + // Would fire an event if we didn't have the flag set + element.checked = checked; + } + }); + } finally { + this._shouldFireCheckedEvents = true; + 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), + 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[]) { + try { + this._shouldFireCheckedEvents = false; + const checked = new Set(); + for (const item of items) { + checked.add(item); + } + for (const element of this._itemElements) { + // Would fire an event if we didn't have the flag set + element.checked = checked.has(element.item); + } + } finally { + this._shouldFireCheckedEvents = true; + 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 reveal its first child so it's not as jarring. + if (this._separatorRenderer.isSeparatorVisible(e.element)) { + this._tree.reveal(e.element.children[0]); + } 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, + }); + } + } + const before = this._tree.getFocus().length; + this._tree.setChildren(null, elements); + // Temporary fix until we figure out why the tree doesn't fire an event when focus & selection + // get changed to empty arrays. + if (before > 0 && elements.length === 0) { + this._onTriggerEmptySelectionOrFocus.fire({ + elements: [] + }); + } + this._tree.layout(); + + this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); + this._onChangedVisibleCount.fire(shownElements.length); + + return true; + } + + toggleCheckbox() { + try { + this._shouldFireCheckedEvents = false; + const elements = this._tree.getFocus().filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement); + const allChecked = this._allVisibleChecked(elements); + for (const element of elements) { + if (!element.checkboxDisabled) { + // Would fire an event if we didn't have the flag set + element.checked = !allChecked; + } + } + } finally { + this._shouldFireCheckedEvents = true; + 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: QuickPickItemElement[], 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() { + if (!this._shouldFireCheckedEvents) { + return; + } + 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/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 47dc660daca..c160bb1fb93 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/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/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 4ccce2c7120..820544bcd69 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -46,6 +46,10 @@ export interface IQuickPickItem { highlights?: IQuickPickItemHighlights; buttons?: readonly IQuickInputButton[]; picked?: boolean; + /** + * Used when we're in multi-select mode. Renders a disabled checkbox. + */ + disabled?: boolean; alwaysShow?: boolean; } @@ -53,6 +57,7 @@ export interface IQuickPickSeparator { type: 'separator'; id?: string; label?: string; + description?: string; ariaLabel?: string; buttons?: readonly IQuickInputButton[]; tooltip?: string | IMarkdownString; @@ -209,6 +214,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 +295,12 @@ export interface IQuickInput extends IDisposable { * @param reason The reason why the quick input was hidden. */ didHide(reason?: QuickInputHideReason): void; + + /** + * Notifies that the quick input will be hidden. + * @param reason The reason why the quick input will be hidden. + */ + willHide(reason?: QuickInputHideReason): void; } export interface IQuickWidget extends IQuickInput { diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index f2a71af5553..216aa3a7f54 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/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/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 529d9d74999..8b85c237151 100644 --- a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IRemoteAuthorityResolverService, IRemoteConnectionData, RemoteConnectionType, ResolvedAuthority, ResolvedOptions, ResolverResult, WebSocketRemoteConnection, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { getRemoteServerRootPath, parseAuthorityWithOptionalPort } from 'vs/platform/remote/common/remoteHosts'; +import { parseAuthorityWithOptionalPort } from 'vs/platform/remote/common/remoteHosts'; export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService { @@ -34,6 +34,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot isWorkbenchOptionsBasedResolution: boolean, connectionToken: Promise | string | undefined, resourceUriProvider: ((uri: URI) => URI) | undefined, + serverBasePath: string | undefined, @IProductService productService: IProductService, @ILogService private readonly _logService: ILogService, ) { @@ -44,7 +45,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot if (resourceUriProvider) { RemoteAuthorities.setDelegate(resourceUriProvider); } - RemoteAuthorities.setServerRootPath(getRemoteServerRootPath(productService)); + RemoteAuthorities.setServerRootPath(productService, serverBasePath); } async resolveAuthority(authority: string): Promise { diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index 9539650dec0..45ebbe8df04 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -9,6 +9,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { isCancellationError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { RemoteAuthorities } from 'vs/base/common/network'; import * as performance from 'vs/base/common/performance'; import { StopWatch } from 'vs/base/common/stopwatch'; import { generateUuid } from 'vs/base/common/uuid'; @@ -17,7 +18,6 @@ import { Client, ISocket, PersistentProtocol, SocketCloseEventType } from 'vs/ba import { ILogService } from 'vs/platform/log/common/log'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { RemoteAuthorityResolverError, RemoteConnection } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; import { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -232,7 +232,7 @@ async function connectToRemoteExtensionHostAgent(opt let socket: ISocket; try { - socket = await createSocket(options.logService, options.remoteSocketFactoryService, options.connectTo, getRemoteServerRootPath(options), `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, connectionTypeToString(connectionType), `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken); + socket = await createSocket(options.logService, options.remoteSocketFactoryService, options.connectTo, RemoteAuthorities.getServerRootPath(), `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, connectionTypeToString(connectionType), `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken); } catch (error) { options.logService.error(`${logPrefix} socketFactory.connect() failed or timed out. Error:`); options.logService.error(error); diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index ccc99953c8d..ccf58f9accb 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -25,15 +25,6 @@ export function getRemoteName(authority: string | undefined): string | undefined return authority.substr(0, pos); } -/** - * The root path to use when accessing the remote server. The path contains the quality and commit of the current build. - * @param product - * @returns - */ -export function getRemoteServerRootPath(product: { quality?: string; commit?: string }): string { - return `/${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`; -} - export function parseAuthorityWithPort(authority: string): { host: string; port: number } { const { host, port } = parseAuthority(authority); if (typeof port === 'undefined') { diff --git a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts index debbe333ae8..9948495f898 100644 --- a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts @@ -11,7 +11,6 @@ import { RemoteAuthorities } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IProductService } from 'vs/platform/product/common/productService'; import { IRemoteAuthorityResolverService, IRemoteConnectionData, RemoteConnectionType, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; import { ElectronRemoteResourceLoader } from 'vs/platform/remote/electron-sandbox/electronRemoteResourceLoader'; export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService { @@ -33,7 +32,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot this._canonicalURIRequests = new Map(); this._canonicalURIProvider = null; - RemoteAuthorities.setServerRootPath(getRemoteServerRootPath(productService)); + RemoteAuthorities.setServerRootPath(productService, undefined); // on the desktop we don't support custom server base paths } resolveAuthority(authority: string): Promise { diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 66defd005d1..087f5858b48 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -6,7 +6,7 @@ import { IpcMainEvent, MessagePortMain } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, DeferredPromise } from 'vs/base/common/async'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -25,6 +25,7 @@ export class SharedProcess extends Disposable { private readonly firstWindowConnectionBarrier = new Barrier(); private utilityProcess: UtilityProcess | undefined = undefined; + private utilityProcessLogListener: IDisposable | undefined = undefined; constructor( private readonly machineId: string, @@ -104,13 +105,10 @@ export class SharedProcess extends Disposable { // all services within have been created. const whenReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); await whenReady.p; + this.utilityProcessLogListener?.dispose(); this.logService.trace('[SharedProcess] Overall ready'); })(); } @@ -131,11 +129,7 @@ export class SharedProcess extends Disposable { // Wait for shared process indicating that IPC connections are accepted const sharedProcessIpcReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); await sharedProcessIpcReady.p; this.logService.trace('[SharedProcess] IPC ready'); @@ -148,6 +142,15 @@ export class SharedProcess extends Disposable { private createUtilityProcess(): void { this.utilityProcess = this._register(new UtilityProcess(this.logService, NullTelemetryService, this.lifecycleMainService)); + // Install a log listener for very early shared process warnings and errors + this.utilityProcessLogListener = this.utilityProcess.onMessage((e: any) => { + if (typeof e.warning === 'string') { + this.logService.warn(e.warning); + } else if (typeof e.error === 'string') { + this.logService.error(e.error); + } + }); + const inspectParams = parseSharedProcessDebugPort(this.environmentMainService.args, this.environmentMainService.isBuilt); let execArgv: string[] | undefined = undefined; if (inspectParams.port) { diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index debdaf7a926..8c6575f872e 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -175,7 +175,7 @@ export interface ICommandDetectionCapability { readonly currentCommand: ICurrentPartialCommand | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; - readonly onCommandExecuted: Event; + readonly onCommandExecuted: Event; readonly onCommandInvalidated: Event; readonly onCurrentCommandInvalidated: Event; setCwd(value: string): void; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index df3cb4ce9ee..0d4309f98dd 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -74,7 +74,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe readonly onBeforeCommandFinished = this._onBeforeCommandFinished.event; private readonly _onCommandFinished = this._register(new Emitter()); readonly onCommandFinished = this._onCommandFinished.event; - private readonly _onCommandExecuted = this._register(new Emitter()); + private readonly _onCommandExecuted = this._register(new Emitter()); readonly onCommandExecuted = this._onCommandExecuted.event; private readonly _onCommandInvalidated = this._register(new Emitter()); readonly onCommandInvalidated = this._onCommandInvalidated.event; @@ -408,7 +408,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe interface ICommandDetectionHeuristicsHooks { readonly onCurrentCommandInvalidatedEmitter: Emitter; readonly onCommandStartedEmitter: Emitter; - readonly onCommandExecutedEmitter: Emitter; + readonly onCommandExecutedEmitter: Emitter; readonly dimensions: ITerminalDimensions; readonly isCommandStorageDisabled: boolean; @@ -495,7 +495,7 @@ class UnixPtyHeuristics extends Disposable { if (y === commandExecutedLine) { currentCommand.command += this._terminal.buffer.active.getLine(commandExecutedLine)?.translateToString(true, undefined, currentCommand.commandExecutedX) || ''; } - this._hooks.onCommandExecutedEmitter.fire(); + this._hooks.onCommandExecutedEmitter.fire(currentCommand as ITerminalCommand); } } @@ -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. @@ -740,7 +733,7 @@ class WindowsPtyHeuristics extends Disposable { this._onCursorMoveListener.clear(); this._evaluateCommandMarkers(); this._capability.currentCommand.commandExecutedX = this._terminal.buffer.active.cursorX; - this._hooks.onCommandExecutedEmitter.fire(); + this._hooks.onCommandExecutedEmitter.fire(this._capability.currentCommand as ITerminalCommand); this._logService.debug('CommandDetectionCapability#handleCommandExecuted', this._capability.currentCommand.commandExecutedX, this._capability.currentCommand.commandExecutedMarker?.line); } @@ -834,7 +827,7 @@ class WindowsPtyHeuristics extends Disposable { } this._capability.currentCommand.commandExecutedMarker = this._hooks.commandMarkers[this._hooks.commandMarkers.length - 1]; // Fire this now to prevent issues like #197409 - this._hooks.onCommandExecutedEmitter.fire(); + this._hooks.onCommandExecutedEmitter.fire(this._capability.currentCommand as ITerminalCommand); } private _cursorOnNextLine(): boolean { diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 2a2df0a7a44..3a27c6d7a0c 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/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; @@ -611,6 +613,12 @@ export interface IShellLaunchConfig { */ isTransient?: boolean; + /** + * Attempt to force shell integration to be enabled by bypassing the {@link isFeatureTerminal} + * equals false requirement. + */ + forceShellIntegration?: boolean; + /** * Create a terminal without shell integration even when it's enabled */ diff --git a/src/vs/platform/terminal/common/terminalEnvironment.ts b/src/vs/platform/terminal/common/terminalEnvironment.ts index 38e8fa2c669..5ddf2aa71d6 100644 --- a/src/vs/platform/terminal/common/terminalEnvironment.ts +++ b/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { OperatingSystem, OS } from 'vs/base/common/platform'; +import type { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; /** * Aggressively escape non-windows paths to prepare for being sent to a shell. This will do some @@ -59,3 +60,11 @@ export function sanitizeCwd(cwd: string): string { } return cwd; } + +/** + * Determines whether the given shell launch config should use the environment variable collection. + * @param slc The shell launch config to check. + */ +export function shouldUseEnvironmentVariableCollection(slc: IShellLaunchConfig): boolean { + return !slc.strictEnv; +} diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 5f5dc956aa5..798fc50f934 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -97,7 +97,10 @@ const enum VSCodeOscPt { /** * Explicitly set the command line. This helps workaround performance and reliability problems * with parsing out the command, such as conpty not guaranteeing the position of the sequence or - * the shell not guaranteeing that the entire command is even visible. + * the shell not guaranteeing that the entire command is even visible. Ideally this is called + * immediately before {@link CommandExecuted}, immediately before {@link CommandFinished} will + * also work but that means terminal will only know the accurate command line when the command is + * finished. * * The command line can escape ascii characters using the `\xAB` format, where AB are the * hexadecimal representation of the character code (case insensitive), and escape the `\` diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 71a72ae4fef..c5e79c22ae3 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/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.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/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index c0948533749..82b65f7a795 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/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/src/vs/platform/theme/common/colorUtils.ts b/src/vs/platform/theme/common/colorUtils.ts new file mode 100644 index 00000000000..2388e7cb702 --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/baseColors.ts b/src/vs/platform/theme/common/colors/baseColors.ts new file mode 100644 index 00000000000..1d19b3adc1f --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/chartsColors.ts b/src/vs/platform/theme/common/colors/chartsColors.ts new file mode 100644 index 00000000000..eb63b602234 --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/editorColors.ts b/src/vs/platform/theme/common/colors/editorColors.ts new file mode 100644 index 00000000000..4116f5ec141 --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/inputColors.ts b/src/vs/platform/theme/common/colors/inputColors.ts new file mode 100644 index 00000000000..dc38222d402 --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/listColors.ts b/src/vs/platform/theme/common/colors/listColors.ts new file mode 100644 index 00000000000..b6f51e3696b --- /dev/null +++ b/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, transparent, ifDefinedThenElse } 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/src/vs/platform/theme/common/colors/menuColors.ts b/src/vs/platform/theme/common/colors/menuColors.ts new file mode 100644 index 00000000000..6fa9a0ec326 --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/minimapColors.ts b/src/vs/platform/theme/common/colors/minimapColors.ts new file mode 100644 index 00000000000..0b051994d09 --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/miscColors.ts b/src/vs/platform/theme/common/colors/miscColors.ts new file mode 100644 index 00000000000..5a2ea49b702 --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/quickpickColors.ts b/src/vs/platform/theme/common/colors/quickpickColors.ts new file mode 100644 index 00000000000..7f8fc271a6e --- /dev/null +++ b/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/src/vs/platform/theme/common/colors/searchColors.ts b/src/vs/platform/theme/common/colors/searchColors.ts new file mode 100644 index 00000000000..8f10c53ab0e --- /dev/null +++ b/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/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index 282230adee5..214f4846dcc 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/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/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 62c9059d2f8..86b4da4b409 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/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/src/vs/platform/tunnel/test/common/tunnel.test.ts b/src/vs/platform/tunnel/test/common/tunnel.test.ts new file mode 100644 index 00000000000..d86d3f47bd7 --- /dev/null +++ b/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/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 4cc8994bd01..73e7d7afffe 100644 --- a/src/vs/platform/update/common/update.ts +++ b/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/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 329488abb51..183c69da906 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -24,8 +24,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } - @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version) => ({ url, version, productVersion: version })); } - @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, date) => ({ releaseNotes, version, productVersion: version, date })); } + @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @@ -96,12 +96,12 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau electron.autoUpdater.checkForUpdates(); } - private onUpdateAvailable(update: IUpdate): void { + private onUpdateAvailable(): void { if (this.state.type !== StateType.CheckingForUpdates) { return; } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); } private onUpdateDownloaded(update: IUpdate): void { @@ -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/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index cf54be65d45..c20ce198e0c 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/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/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index ff8bbb0e559..4c49a758185 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -135,7 +135,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); return this.cleanup(update.version).then(() => { return this.getUpdatePackagePath(update.version).then(updatePackagePath => { @@ -153,15 +153,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun .then(() => updatePackagePath); }); }).then(packagePath => { - const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); - this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); @@ -209,7 +207,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async doApplyUpdate(): Promise { - if (this.state.type !== StateType.Downloaded && this.state.type !== StateType.Downloading) { + if (this.state.type !== StateType.Downloaded) { return Promise.resolve(undefined); } @@ -273,14 +271,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); const update: IUpdate = { version: 'unknown', productVersion: 'unknown' }; - this.setState(State.Downloading(update)); + this.setState(State.Downloading); this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index 5a532c8bae2..5656877af53 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -187,6 +187,11 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i if (localChange !== Change.None) { await this.backupLocal(stringifyLocalProfiles(this.getLocalUserDataProfiles(), false)); + await Promise.all(local.removed.map(async profile => { + this.logService.trace(`${this.syncResourceLogLabel}: Removing '${profile.name}' profile...`); + await this.userDataProfilesService.removeProfile(profile); + this.logService.info(`${this.syncResourceLogLabel}: Removed profile '${profile.name}'.`); + })); const promises: Promise[] = []; for (const profile of local.added) { promises.push((async () => { @@ -195,13 +200,6 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i this.logService.info(`${this.syncResourceLogLabel}: Created profile '${profile.name}'.`); })()); } - for (const profile of local.removed) { - promises.push((async () => { - this.logService.trace(`${this.syncResourceLogLabel}: Removing '${profile.name}' profile...`); - await this.userDataProfilesService.removeProfile(profile); - this.logService.info(`${this.syncResourceLogLabel}: Removed profile '${profile.name}'.`); - })()); - } for (const profile of local.updated) { const localProfile = this.userDataProfilesService.profiles.find(p => p.id === profile.id); if (localProfile) { diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 6bd1d52b986..8386e94dab2 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -212,7 +212,10 @@ export class UtilityProcess extends Disposable { const started = this.doStart(configuration); if (started && configuration.payload) { - this.postMessage(configuration.payload); + const posted = this.postMessage(configuration.payload); + if (posted) { + this.log('payload sent via postMessage()', Severity.Info); + } } return started; @@ -363,12 +366,14 @@ export class UtilityProcess extends Disposable { })); } - postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): void { + postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): boolean { if (!this.process) { - return; // already killed, crashed or never started + return false; // already killed, crashed or never started } this.process.postMessage(message, transfer); + + return true; } connect(payload?: unknown): Electron.MessagePortMain { diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index f0d8bac84c0..08c7e94055a 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -989,6 +989,13 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + type appWithProxySupport = Electron.App & { + setProxy(config: Electron.Config): Promise; + resolveProxy(url: string): Promise; + }; + if (typeof (app as appWithProxySupport).setProxy === 'function') { + (app as appWithProxySupport).setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + } } } } diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index a90d28e82cf..84664bbb39a 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -15,7 +15,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { isEqualOrParent } from 'vs/base/common/extpath'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { connectionTokenQueryName, FileAccess, Schemas } from 'vs/base/common/network'; +import { connectionTokenQueryName, FileAccess, getServerRootPath, Schemas } from 'vs/base/common/network'; import { dirname, join } from 'vs/base/common/path'; import * as perf from 'vs/base/common/performance'; import * as platform from 'vs/base/common/platform'; @@ -33,7 +33,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { ConnectionType, ConnectionTypeRequest, ErrorMessage, HandshakeMessage, IRemoteExtensionHostStartParams, ITunnelConnectionStartParams, SignRequest } from 'vs/platform/remote/common/remoteAgentConnection'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionHostConnection } from 'vs/server/node/extensionHostConnection'; import { ManagementConnection } from 'vs/server/node/remoteExtensionManagement'; @@ -75,6 +74,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { private readonly _connectionToken: ServerConnectionToken, private readonly _vsdaMod: typeof vsda | null, hasWebClient: boolean, + serverBasePath: string | undefined, @IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService, @IProductService private readonly _productService: IProductService, @ILogService private readonly _logService: ILogService, @@ -82,13 +82,13 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { ) { super(); - this._serverRootPath = getRemoteServerRootPath(_productService); + this._serverRootPath = getServerRootPath(_productService, serverBasePath); this._extHostConnections = Object.create(null); this._managementConnections = Object.create(null); this._allReconnectionTokens = new Set(); this._webClientServer = ( hasWebClient - ? this._instantiationService.createInstance(WebClientServer, this._connectionToken) + ? this._instantiationService.createInstance(WebClientServer, this._connectionToken, serverBasePath ?? '/', this._serverRootPath) : null ); this._logService.info(`Extension host agent started.`); @@ -665,6 +665,7 @@ export interface IServerAPI { } export async function createServer(address: string | net.AddressInfo | null, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise { + const connectionToken = await determineServerConnectionToken(args); if (connectionToken instanceof ServerConnectionTokenParseError) { console.warn(connectionToken.message); @@ -774,15 +775,20 @@ export async function createServer(address: string | net.AddressInfo | null, arg return null; }); + let serverBasePath = args['server-base-path']; + if (serverBasePath && !serverBasePath.startsWith('/')) { + serverBasePath = `/${serverBasePath}`; + } + const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/code/browser/workbench/workbench.html').fsPath); if (hasWebClient && address && typeof address !== 'string') { // ships the web ui! const queryPart = (connectionToken.type !== ServerConnectionTokenType.None ? `?${connectionTokenQueryName}=${connectionToken.value}` : ''); - console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}/${queryPart}`); + console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}${serverBasePath ?? ''}${queryPart}`); } - const remoteExtensionHostAgentServer = instantiationService.createInstance(RemoteExtensionHostAgentServer, socketServer, connectionToken, vsdaMod, hasWebClient); + const remoteExtensionHostAgentServer = instantiationService.createInstance(RemoteExtensionHostAgentServer, socketServer, connectionToken, vsdaMod, hasWebClient, serverBasePath); perf.mark('code/server/ready'); const currentTime = performance.now(); diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index caec44f7f3a..657d3e8238a 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -32,6 +32,7 @@ import { IExtensionManagementService } from 'vs/platform/extensionManagement/com import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { promiseWithResolvers } from 'vs/base/common/async'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( @@ -235,7 +236,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< ); // Apply extension environment variable collections to the environment - if (!shellLaunchConfig.strictEnv) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const entries: [string, IEnvironmentVariableCollection][] = []; for (const [k, v, d] of args.envVariableCollections) { entries.push([k, { map: deserializeEnvironmentVariableCollection(v), descriptionMap: deserializeEnvironmentDescriptionMap(d) }]); diff --git a/src/vs/server/node/serverEnvironmentService.ts b/src/vs/server/node/serverEnvironmentService.ts index 900815a06d2..fce1842f1bd 100644 --- a/src/vs/server/node/serverEnvironmentService.ts +++ b/src/vs/server/node/serverEnvironmentService.ts @@ -19,6 +19,7 @@ export const serverOptions: OptionDescriptions> = { 'host': { type: 'string', cat: 'o', args: 'ip-address', description: nls.localize('host', "The host name or IP address the server should listen to. If not set, defaults to 'localhost'.") }, 'port': { type: 'string', cat: 'o', args: 'port | port range', description: nls.localize('port', "The port the server should listen to. If 0 is passed a random free port is picked. If a range in the format num-num is passed, a free port from the range (end inclusive) is selected.") }, 'socket-path': { type: 'string', cat: 'o', args: 'path', description: nls.localize('socket-path', "The path to a socket file for the server to listen to.") }, + 'server-base-path': { type: 'string', cat: 'o', args: 'path', description: nls.localize('server-base-path', "The path under which the web UI and the code server is provided. Defaults to '/'.`") }, 'connection-token': { type: 'string', cat: 'o', args: 'token', deprecates: ['connectionToken'], description: nls.localize('connection-token', "A secret that must be included with all requests.") }, 'connection-token-file': { type: 'string', cat: 'o', args: 'path', deprecates: ['connection-secret', 'connectionTokenFile'], description: nls.localize('connection-token-file', "Path to a file that contains the connection token.") }, 'without-connection-token': { type: 'boolean', cat: 'o', description: nls.localize('without-connection-token', "Run without a connection token. Only use this if the connection is secured by other means.") }, @@ -102,6 +103,12 @@ export interface ServerParsedArgs { port?: string; 'socket-path'?: string; + /** + * The path under which the web UI and the code server is provided. + * By defaults it is '/'.` + */ + 'server-base-path'?: string; + /** * A secret token that must be provided by the web client with all requests. * Use only `[0-9A-Za-z\-]`. diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 413b06f6aec..84c7748356e 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -28,7 +28,6 @@ import { streamToBuffer } from 'vs/base/common/buffer'; import { IProductConfiguration } from 'vs/base/common/product'; import { isString } from 'vs/base/common/types'; import { CharCode } from 'vs/base/common/charCode'; -import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; const textMimeType = { @@ -104,13 +103,15 @@ export class WebClientServer { constructor( private readonly _connectionToken: ServerConnectionToken, + private readonly _basePath: string, + readonly serverRootPath: string, @IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService, @ILogService private readonly _logService: ILogService, @IRequestService private readonly _requestService: IRequestService, @IProductService private readonly _productService: IProductService, ) { this._webExtensionResourceUrlTemplate = this._productService.extensionsGallery?.resourceUrlTemplate ? URI.parse(this._productService.extensionsGallery.resourceUrlTemplate) : undefined; - const serverRootPath = getRemoteServerRootPath(_productService); + this._staticRoute = `${serverRootPath}/static`; this._callbackRoute = `${serverRootPath}/callback`; this._webExtensionRoute = `${serverRootPath}/web-extension-resource`; @@ -128,7 +129,7 @@ export class WebClientServer { if (pathname.startsWith(this._staticRoute) && pathname.charCodeAt(this._staticRoute.length) === CharCode.Slash) { return this._handleStatic(req, res, parsedUrl); } - if (pathname === '/') { + if (pathname === this._basePath) { return this._handleRoot(req, res, parsedUrl); } if (pathname === this._callbackRoute) { @@ -262,7 +263,7 @@ export class WebClientServer { newQuery[key] = parsedUrl.query[key]; } } - const newLocation = url.format({ pathname: '/', query: newQuery }); + const newLocation = url.format({ pathname: parsedUrl.pathname, query: newQuery }); responseHeaders['Location'] = newLocation; res.writeHead(302, responseHeaders); @@ -336,6 +337,7 @@ export class WebClientServer { const workbenchWebConfiguration = { remoteAuthority, + serverBasePath: this._basePath, _wrapWebWorkerExtHostInIframe, developmentOptions: { enableSmokeTestDriver: this._environmentService.args['enable-smoke-test-driver'] ? true : undefined, logLevel: this._logService.getLevel() }, settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index d67e655e9c0..734ab1db7e5 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -11,13 +11,13 @@ import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValida import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint'; import { IconExtensionPoint } from 'vs/workbench/services/themes/common/iconExtensionPoint'; import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes/common/tokenClassificationExtensionPoint'; -import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint'; +import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; import { StatusBarItemsExtensionPoint } from 'vs/workbench/api/browser/statusBarExtensionPoint'; // --- mainThread participants import './mainThreadLocalization'; import './mainThreadBulkEdits'; -import './mainThreadChatProvider'; +import './mainThreadLanguageModels'; import './mainThreadChatAgents2'; import './mainThreadChatVariables'; import './mainThreadCodeInsets'; @@ -59,6 +59,7 @@ import './mainThreadStatusBar'; import './mainThreadStorage'; import './mainThreadTelemetry'; import './mainThreadTerminalService'; +import './mainThreadTerminalShellIntegration'; import './mainThreadTheming'; import './mainThreadTreeViews'; import './mainThreadDownloadService'; @@ -84,7 +85,7 @@ import './mainThreadTimeline'; import './mainThreadTesting'; import './mainThreadSecretState'; import './mainThreadShare'; -import './mainThreadProfilContentHandlers'; +import './mainThreadProfileContentHandlers'; import './mainThreadAiRelatedInformation'; import './mainThreadAiEmbeddingVector'; import './mainThreadIssueReporter'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 22c82811bb4..b3ebdd940c3 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,18 +6,32 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { getAuthenticationProviderActivationEvent, addAccountUsage } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import type { AuthenticationGetSessionOptions } from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +interface AuthenticationForceNewSessionOptions { + detail?: string; + learnMore?: UriComponents; + sessionToRecreate?: AuthenticationSession; +} +interface AuthenticationGetSessionOptions { + clearSessionPreference?: boolean; + createIfNone?: boolean; + forceNewSession?: boolean | AuthenticationForceNewSessionOptions; + silent?: boolean; +} export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -58,11 +72,14 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu constructor( extHostContext: IExtHostContext, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, + @IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService, + @IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -100,23 +117,43 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu $removeSession(providerId: string, sessionId: string): Promise { return this.authenticationService.removeSession(providerId, sessionId); } - private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, detail?: string): Promise { + private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { const message = recreatingSession ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName) : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName); - const { confirmed } = await this.dialogService.confirm({ + + const buttons: IPromptButton[] = [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run() { + return true; + }, + } + ]; + if (options?.learnMore) { + buttons.push({ + label: nls.localize('learnMore', "Learn more"), + run: async () => { + const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); + return await result; + } + }); + } + const { result } = await this.dialogService.prompt({ type: Severity.Info, message, - detail, - primaryButton: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow") + buttons, + detail: options?.detail, + cancelButton: true, }); - return confirmed; + return result ?? false; } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { const sessions = await this.authenticationService.getSessions(providerId, scopes, true); - const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId); + const provider = this.authenticationService.getProvider(providerId); // Error cases if (options.forceNewSession && options.createIfNone) { @@ -131,22 +168,22 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { - if (supportsMultipleAccounts) { + if (provider.supportsMultipleAccounts) { if (options.clearSessionPreference) { // Clearing the session preference is usually paired with createIfNone, so just remove the preference and // defer to the rest of the logic in this function to choose the session. - this.authenticationService.removeSessionPreference(providerId, extensionId, scopes); + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); } else { // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); if (existingSessionPreference) { const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { return matchingSession; } } } - } else if (this.authenticationService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { + } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { return sessions[0]; } } @@ -154,51 +191,44 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We may need to prompt because we don't have a valid session // modal flows if (options.createIfNone || options.forceNewSession) { - const providerName = this.authenticationService.getLabel(providerId); - const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined; + let uiOptions: AuthenticationForceNewSessionOptions | undefined; + if (typeof options.forceNewSession === 'object') { + uiOptions = options.forceNewSession; + } // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(providerName, extensionName, recreatingSession, detail); + const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } let session; if (sessions?.length && !options.forceNewSession) { - session = supportsMultipleAccounts - ? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions) + session = provider.supportsMultipleAccounts + ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { let sessionToRecreate: AuthenticationSession | undefined; if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; } else { - const sessionIdToRecreate = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; } session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); } - this.authenticationService.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, session); + this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.authenticationExtensionsService.updateSessionPreference(providerId, extensionId, session); return session; } // For the silent flows, if we have a session, even though it may not be the user's preference, we'll return it anyway because it might be for a specific // set of scopes. - const validSession = sessions.find(session => this.authenticationService.isAccessAllowed(providerId, session.account.label, extensionId)); + const validSession = sessions.find(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId)); if (validSession) { - // Migration. If we have a valid session, but no preference, we'll set the preference to the valid session. - // TODO: Remove this after in a few releases. - if (!this.authenticationService.getSessionPreference(providerId, extensionId, scopes)) { - if (this.storageService.get(`${extensionName}-${providerId}`, StorageScope.APPLICATION)) { - this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.APPLICATION); - } - this.authenticationService.updateAllowedExtension(providerId, validSession.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, validSession); - } return validSession; } @@ -207,8 +237,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow, // otherwise request a new one. sessions.length - ? this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) - : await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); + ? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) + : await this.authenticationExtensionsService.requestNewSession(providerId, scopes, extensionId, extensionName); } return undefined; } @@ -218,7 +248,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu if (session) { this.sendProviderUsageTelemetry(extensionId, providerId); - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } return session; @@ -226,11 +256,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationService.isAccessAllowed(providerId, s.account.label, extensionId)); + const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); if (accessibleSessions.length) { this.sendProviderUsageTelemetry(extensionId, providerId); for (const session of accessibleSessions) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } } return accessibleSessions; diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index c7155e716a9..1a4d657cd94 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostChatShape, ExtHostContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadChat) @@ -55,18 +55,10 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { return undefined; } - const responderAvatarIconUri = session.responderAvatarIconUri && - URI.revive(session.responderAvatarIconUri); - const emitter = new Emitter(); this._stateEmitters.set(session.id, emitter); return { id: session.id, - requesterUsername: session.requesterUsername, - requesterAvatarIconUri: URI.revive(session.requesterAvatarIconUri), - responderUsername: session.responderUsername, - responderAvatarIconUri, - inputPlaceholder: session.inputPlaceholder, dispose: () => { emitter.dispose(); this._stateEmitters.delete(session.id); @@ -83,13 +75,6 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { this._stateEmitters.get(sessionId)?.fire(state); } - async $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(providerId); - if (widget && widget.viewModel) { - this._chatService.sendRequestToProvider(widget.viewModel.sessionId, message); - } - } - async $unregisterChatProvider(handle: number): Promise { this._providerRegistrations.deleteAndDispose(handle); } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index c143be65078..0aaaee8cf14 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -19,19 +19,18 @@ 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 { 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'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; -type AgentData = { +interface AgentData { dispose: () => void; - name: string; - hasSlashCommands?: boolean; + id: string; + extensionId: ExtensionIdentifier; hasFollowups?: boolean; -}; +} @extHostNamedCustomer(MainContext.MainThreadChatAgents2) export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 { @@ -59,7 +58,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._register(this._chatService.onDidPerformUserAction(e => { if (typeof e.agentId === 'string') { for (const [handle, agent] of this._agents) { - if (agent.name === e.agentId) { + if (agent.id === e.agentId) { if (e.action.kind === 'vote') { this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction); } else { @@ -76,12 +75,19 @@ 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, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void { + const staticAgentRegistration = this._chatAgentService.getAgent(id); + if (!staticAgentRegistration && !dynamicProps) { + if (this._chatAgentService.getAgentsByName(id).length) { + // Likely some extension authors will not adopt the new ID, so give a hint if they register a + // participant by name instead of ID. + throw new Error(`chatParticipant must be declared with an ID in package.json. The "id" property may be missing! "${id}"`); + } + + throw new Error(`chatParticipant must be declared in package.json: ${id}`); + } + + const impl: IChatAgentImplementation = { invoke: async (request, progress, history, token) => { this._pendingProgress.set(request.requestId, progress); try { @@ -90,26 +96,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 +109,29 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA provideSampleQuestions: (token: CancellationToken) => { return this._proxy.$provideSampleQuestions(handle, token); } - }); + }; + + let disposable: IDisposable; + if (!staticAgentRegistration && dynamicProps) { + disposable = this._chatAgentService.registerDynamicAgent( + { + id, + name: dynamicProps.name, + description: dynamicProps.description, + extensionId: extension, + metadata: revive(metadata), + slashCommands: [], + locations: [ChatAgentLocation.Panel] // TODO all dynamic participants are panel only? + }, + impl); + } else { + disposable = this._chatAgentService.registerAgentImplementation(id, impl); + } + this._agents.set(handle, { - name, - dispose: d.dispose, - hasSlashCommands: metadata.hasSlashCommands, + id: id, + extensionId: extension, + dispose: disposable.dispose, hasFollowups: metadata.hasFollowups }); } @@ -131,9 +141,8 @@ 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)); + this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); } async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise { @@ -161,8 +170,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()).parts; const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); - const thisAgentName = this._agents.get(handle)?.name; - if (agentPart?.agent.id !== thisAgentName) { + const thisAgentId = this._agents.get(handle)?.id; + if (agentPart?.agent.id !== thisAgentId) { return; } diff --git a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index 4ad9e654807..3fa0eef969f 100644 --- a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts +++ b/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/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 538c754e1b3..5bfadbbc409 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/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/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index d32e4ef318e..3f992a4cfad 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/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/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 3178df6d093..e5dfc1a814e 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -5,7 +5,7 @@ import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI as uri, UriComponents } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto @@ -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/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index d23f8e91fd5..e0f966a9fb2 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/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/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 20c6c76910d..22976049d5a 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/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/src/vs/workbench/api/browser/mainThreadInlineChat.ts b/src/vs/workbench/api/browser/mainThreadInlineChat.ts index 60c2ab5fb80..48207f86ffa 100644 --- a/src/vs/workbench/api/browser/mainThreadInlineChat.ts +++ b/src/vs/workbench/api/browser/mainThreadInlineChat.ts @@ -10,6 +10,7 @@ import { reviveWorkspaceEditDto } from 'vs/workbench/api/browser/mainThreadBulkE import { ExtHostContext, ExtHostInlineChatShape, MainContext, MainThreadInlineChatShape as MainThreadInlineChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IProgress } from 'vs/platform/progress/common/progress'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @extHostNamedCustomer(MainContext.MainThreadInlineChat) export class MainThreadInlineChat implements MainThreadInlineChatShape { @@ -31,9 +32,9 @@ export class MainThreadInlineChat implements MainThreadInlineChatShape { this._registrations.dispose(); } - async $registerInteractiveEditorProvider(handle: number, label: string, debugName: string, supportsFeedback: boolean, supportsFollowups: boolean, supportIssueReporting: boolean): Promise { + async $registerInteractiveEditorProvider(handle: number, label: string, extensionId: ExtensionIdentifier, supportsFeedback: boolean, supportsFollowups: boolean, supportIssueReporting: boolean): Promise { const unreg = this._inlineChatService.addProvider({ - debugName, + extensionId, label, supportIssueReporting, prepareInlineChatSession: async (model, range, token) => { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c0bf56701fe..a814d6f9bff 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/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 => { @@ -981,8 +982,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), @@ -1010,23 +1011,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 => { @@ -1057,20 +1058,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 { @@ -1082,21 +1104,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); @@ -1104,15 +1123,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/src/vs/workbench/api/browser/mainThreadChatProvider.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts similarity index 76% rename from src/vs/workbench/api/browser/mainThreadChatProvider.ts rename to src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 3a2d81c51aa..9a886a6713a 100644 --- a/src/vs/workbench/api/browser/mainThreadChatProvider.ts +++ b/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'; @@ -11,33 +12,35 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ExtHostChatProviderShape, ExtHostContext, MainContext, MainThreadChatProviderShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IChatResponseProviderMetadata, IChatResponseFragment, IChatProviderService, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; +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'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -@extHostNamedCustomer(MainContext.MainThreadChatProvider) -export class MainThreadChatProvider implements MainThreadChatProviderShape { +@extHostNamedCustomer(MainContext.MainThreadLanguageModels) +export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { - private readonly _proxy: ExtHostChatProviderShape; + private readonly _proxy: ExtHostLanguageModelsShape; private readonly _store = new DisposableStore(); private readonly _providerRegistrations = new DisposableMap(); private readonly _pendingProgress = new Map>(); constructor( extHostContext: IExtHostContext, - @IChatProviderService private readonly _chatProviderService: IChatProviderService, + @ILanguageModelsService private readonly _chatProviderService: ILanguageModelsService, @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.getProviders() }); - this._store.add(_chatProviderService.onDidChangeProviders(this._proxy.$updateLanguageModels, this._proxy)); + this._proxy.$updateLanguageModels({ added: coalesce(_chatProviderService.getLanguageModelIds().map(id => _chatProviderService.lookupLanguageModel(id))) }); + this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$updateLanguageModels, this._proxy)); } dispose(): void { @@ -45,9 +48,9 @@ export class MainThreadChatProvider implements MainThreadChatProviderShape { this._store.dispose(); } - $registerProvider(handle: number, identifier: string, metadata: IChatResponseProviderMetadata): void { + $registerLanguageModelProvider(handle: number, identifier: string, metadata: ILanguageModelChatMetadata): void { const dipsosables = new DisposableStore(); - dipsosables.add(this._chatProviderService.registerChatResponseProvider(identifier, { + dipsosables.add(this._chatProviderService.registerLanguageModelChat(identifier, { metadata, provideChatResponse: async (messages, from, options, progress, token) => { const requestId = (Math.random() * 1e6) | 0; @@ -80,10 +83,10 @@ export class MainThreadChatProvider implements MainThreadChatProviderShape { this._providerRegistrations.deleteAndDispose(handle); } - async $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise { + async $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise { const activate = this._extensionService.activateByEvent(`onLanguageModelAccess:${providerId}`); - const metadata = this._chatProviderService.lookupChatResponseProvider(providerId); + const metadata = this._chatProviderService.lookupLanguageModel(providerId); if (metadata) { return metadata; @@ -91,10 +94,10 @@ export class MainThreadChatProvider implements MainThreadChatProviderShape { await Promise.race([ activate, - Event.toPromise(Event.filter(this._chatProviderService.onDidChangeProviders, 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.lookupChatResponseProvider(providerId); + return this._chatProviderService.lookupLanguageModel(providerId); } async $fetchResponse(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { @@ -102,7 +105,7 @@ export class MainThreadChatProvider implements MainThreadChatProviderShape { this._logService.debug('[CHAT] extension request STARTED', extension.value, requestId); - const task = this._chatProviderService.fetchChatResponse(providerId, extension, messages, options, new Progress(value => { + const task = this._chatProviderService.makeLanguageModelChatRequest(providerId, extension, messages, options, new Progress(value => { this._proxy.$handleResponseFragment(requestId, value); }), token); @@ -131,28 +134,8 @@ export class MainThreadChatProvider implements MainThreadChatProviderShape { disposables.add(toDisposable(() => { this._authenticationService.unregisterAuthenticationProvider(authProviderId); })); - disposables.add(this._authenticationService.onDidChangeSessions(async (e) => { - if (e.providerId === authProviderId) { - if (e.event.removed?.length) { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); - const extensionsToUpdateAccess = []; - for (const allowed of allowedExtensions) { - const from = await this._extensionService.getExtension(allowed.id); - this._authenticationService.updateAllowedExtension(authProviderId, authProviderId, allowed.id, allowed.name, false); - if (from) { - extensionsToUpdateAccess.push({ - from: from.identifier, - to: extension, - enabled: false - }); - } - } - this._proxy.$updateModelAccesslist(extensionsToUpdateAccess); - } - } - })); - disposables.add(this._authenticationService.onDidChangeExtensionSessionAccess(async (e) => { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); + disposables.add(this._authenticationAccessService.onDidChangeExtensionSessionAccess(async (e) => { + const allowedExtensions = this._authenticationAccessService.readAllowedExtensions(authProviderId, accountLabel); const accessList = []; for (const allowedExtension of allowedExtensions) { const from = await this._extensionService.getExtension(allowedExtension.id); diff --git a/src/vs/workbench/api/browser/mainThreadProfilContentHandlers.ts b/src/vs/workbench/api/browser/mainThreadProfileContentHandlers.ts similarity index 100% rename from src/vs/workbench/api/browser/mainThreadProfilContentHandlers.ts rename to src/vs/workbench/api/browser/mainThreadProfileContentHandlers.ts diff --git a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index 48f53faea39..d5312097ee9 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/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/src/vs/workbench/api/browser/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index 797a4ee92a4..9b1af87b574 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/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/src/vs/workbench/api/browser/mainThreadShare.ts b/src/vs/workbench/api/browser/mainThreadShare.ts index 1974180b331..d517c23c906 100644 --- a/src/vs/workbench/api/browser/mainThreadShare.ts +++ b/src/vs/workbench/api/browser/mainThreadShare.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 { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ExtHostContext, ExtHostShareShape, IDocumentFilterDto, MainContext, MainThreadShareShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -31,7 +31,7 @@ export class MainThreadShare implements MainThreadShareShape { selector, priority, provideShare: async (item: IShareableItem) => { - const result = await this.proxy.$provideShare(handle, item, new CancellationTokenSource().token); + const result = await this.proxy.$provideShare(handle, item, CancellationToken.None); return typeof result === 'string' ? result : URI.revive(result); } }; diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 2293708c63b..cc08f3cdf7c 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -25,7 +25,6 @@ import { ITerminalLinkProviderService } from 'vs/workbench/contrib/terminalContr import { ITerminalQuickFixService, ITerminalQuickFix, TerminalQuickFixType } from 'vs/workbench/contrib/terminalContrib/quickFix/browser/quickFix'; import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; - @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -152,6 +151,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape ? (id, cols, rows) => new TerminalProcessExtHostProxy(id, cols, rows, this._terminalService) : undefined, extHostTerminalId, + forceShellIntegration: launchConfig.forceShellIntegration, isFeatureTerminal: launchConfig.isFeatureTerminal, isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal, useShellEnvironment: launchConfig.useShellEnvironment, diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts new file mode 100644 index 00000000000..97eefc8b00e --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { TerminalCapability, type ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ExtHostContext, MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { extHostNamedCustomer, type IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadTerminalShellIntegration) +export class MainThreadTerminalShellIntegration extends Disposable implements MainThreadTerminalShellIntegrationShape { + private readonly _proxy: ExtHostTerminalShellIntegrationShape; + + constructor( + extHostContext: IExtHostContext, + @ITerminalService private readonly _terminalService: ITerminalService, + @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService + ) { + super(); + + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalShellIntegration); + + // onDidChangeTerminalShellIntegration + const onDidAddCommandDetection = this._terminalService.createOnInstanceEvent(instance => { + return Event.map( + Event.filter(instance.capabilities.onDidAddCapabilityType, e => { + return e === TerminalCapability.CommandDetection; + }, this._store), () => instance + ); + }); + this._store.add(onDidAddCommandDetection(e => this._proxy.$shellIntegrationChange(e.instanceId))); + + // onDidStartTerminalShellExecution + const commandDetectionStartEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandExecuted)); + let currentCommand: ITerminalCommand | undefined; + this._store.add(commandDetectionStartEvent.event(e => { + // Prevent duplicate events from being sent in case command detection double fires the + // event + if (e.data === currentCommand) { + return; + } + currentCommand = e.data; + this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, e.data.cwd); + })); + + // onDidEndTerminalShellExecution + const commandDetectionEndEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandFinished)); + this._store.add(commandDetectionEndEvent.event(e => { + currentCommand = undefined; + this._proxy.$shellExecutionEnd(e.instance.instanceId, e.data.command, e.data.exitCode); + })); + + // onDidChangeTerminalShellIntegration via cwd + const cwdChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CwdDetection, e => e.onDidChangeCwd)); + this._store.add(cwdChangeEvent.event(e => this._proxy.$cwdChange(e.instance.instanceId, e.data))); + + // Clean up after dispose + this._store.add(this._terminalService.onDidDisposeInstance(e => this._proxy.$closeTerminal(e.instanceId))); + + // TerminalShellExecution.createDataStream + // TODO: Support this on remote; it should go via the server + if (!workbenchEnvironmentService.remoteAuthority) { + this._store.add(this._terminalService.onAnyInstanceData(e => this._proxy.$shellExecutionData(e.instance.instanceId, e.data))); + } + } + + $executeCommand(terminalId: number, commandLine: string): void { + this._terminalService.getInstanceFromId(terminalId)?.runCommand(commandLine, true); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index fa5376ca4b1..a316bec0eb3 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/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'; @@ -19,7 +19,7 @@ import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testPro import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @@ -50,18 +50,28 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh this._register(Event.debounce(testProfiles.onDidChange, (_last, e) => e)(() => { const obj: Record = {}; - for (const { controller, profiles } of this.testProfiles.all()) { - obj[controller.id] = profiles.filter(p => p.isDefault).map(p => p.profileId); + for (const group of [TestRunProfileBitset.Run, TestRunProfileBitset.Debug, TestRunProfileBitset.Coverage]) { + for (const profile of this.testProfiles.getGroupDefaultProfiles(group)) { + obj[profile.controllerId] ??= []; + obj[profile.controllerId].push(profile.profileId); + } } this.proxy.$setDefaultRunProfiles(obj); })); 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 +131,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/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 71ec5250fb9..7f9eae154c3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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 { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider'; +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'; @@ -108,6 +108,7 @@ import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/serv import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; import type * as vscode from 'vscode'; +import { IExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -143,6 +144,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,12 +160,15 @@ 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)); const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, accessor.get(IExtHostDocumentsAndEditors)); const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, accessor.get(IExtHostCommands)); const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, accessor.get(IExtHostTerminalService)); + const extHostTerminalShellIntegration = rpcProtocol.set(ExtHostContext.ExtHostTerminalShellIntegration, accessor.get(IExtHostTerminalShellIntegration)); const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, accessor.get(IExtHostDebugService)); const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, accessor.get(IExtHostSearch)); const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, accessor.get(IExtHostTask)); @@ -196,7 +202,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 +212,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 ExtHostChatProvider(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 +288,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 +543,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); }, @@ -741,6 +748,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'terminalExecuteCommandEvent'); return _asExtensionEvent(extHostTerminalService.onDidExecuteTerminalCommand)(listener, thisArg, disposables); }, + onDidChangeTerminalShellIntegration(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension, 'terminalShellIntegration'); + return _asExtensionEvent(extHostTerminalShellIntegration.onDidChangeTerminalShellIntegration)(listener, thisArg, disposables); + }, + onDidStartTerminalShellExecution(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension, 'terminalShellIntegration'); + return _asExtensionEvent(extHostTerminalShellIntegration.onDidStartTerminalShellExecution)(listener, thisArg, disposables); + }, + onDidEndTerminalShellExecution(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension, 'terminalShellIntegration'); + return _asExtensionEvent(extHostTerminalShellIntegration.onDidEndTerminalShellExecution)(listener, thisArg, disposables); + }, get state() { return extHostWindow.getState(extension); }, @@ -1112,6 +1131,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 +1255,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 +1284,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 +1405,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 +1431,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'); @@ -1417,25 +1441,29 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'mappedEditsProvider'); return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); }, - createChatParticipant(name: string, handler: vscode.ChatExtendedRequestHandler) { + createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) { checkProposedApiEnabled(extension, 'chatParticipant'); - return extHostChatAgents2.createChatAgent(extension, name, handler); + return extHostChatAgents2.createChatAgent(extension, id, handler); }, + createDynamicChatParticipant(id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + checkProposedApiEnabled(extension, 'chatParticipantAdditions'); + return extHostChatAgents2.createDynamicChatAgent(extension, id, name, description, handler); + } }; // 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); + }, + sendChatRequest(languageModel: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: vscode.CancellationToken) { + checkProposedApiEnabled(extension, 'languageModels'); + return extHostLanguageModels.sendChatRequest(extension, languageModel, messages, options, token); } }; @@ -1493,6 +1521,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, @@ -1603,7 +1632,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, @@ -1640,7 +1671,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TextSearchCompleteMessageType: TextSearchCompleteMessageType, DataTransfer: extHostTypes.DataTransfer, DataTransferItem: extHostTypes.DataTransferItem, - CoveredCount: extHostTypes.CoveredCount, + TestCoverageCount: extHostTypes.TestCoverageCount, FileCoverage: extHostTypes.FileCoverage, StatementCoverage: extHostTypes.StatementCoverage, BranchCoverage: extHostTypes.BranchCoverage, @@ -1660,16 +1691,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, @@ -1679,9 +1712,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/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index faf45b596a7..d01a3219f94 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -28,11 +28,16 @@ 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'; +import { IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; 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); @@ -45,6 +50,7 @@ registerSingleton(IExtHostSearch, ExtHostSearch, InstantiationType.Eager); registerSingleton(IExtHostStorage, ExtHostStorage, InstantiationType.Eager); registerSingleton(IExtHostTask, WorkerExtHostTask, InstantiationType.Eager); registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService, InstantiationType.Eager); +registerSingleton(IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration, InstantiationType.Eager); registerSingleton(IExtHostTunnelService, ExtHostTunnelService, InstantiationType.Eager); registerSingleton(IExtHostWindow, ExtHostWindow, InstantiationType.Eager); registerSingleton(IExtHostWorkspace, ExtHostWorkspace, InstantiationType.Eager); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 95b52585273..2c66bd69e94 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,12 +50,12 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { IChatAgentCommand, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider'; -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; @@ -441,7 +442,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { // --- End Positron --- $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; @@ -504,6 +505,7 @@ export interface TerminalLaunchConfig { strictEnv?: boolean; hideFromUser?: boolean; isExtensionCustomPtyTerminal?: boolean; + forceShellIntegration?: boolean; isFeatureTerminal?: boolean; isExtensionOwnedTerminal?: boolean; useShellEnvironment?: boolean; @@ -539,6 +541,10 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $sendProcessExit(terminalId: number, exitCode: number | undefined): void; } +export interface MainThreadTerminalShellIntegrationShape extends IDisposable { + $executeCommand(terminalId: number, commandLine: string): void; +} + export type TransferQuickPickItemOrSeparator = TransferQuickPickItem | quickInput.IQuickPickSeparator; export interface TransferQuickPickItem { handle: number; @@ -701,7 +707,8 @@ export const enum TabInputKind { WebviewEditorInput, TerminalEditorInput, InteractiveEditorInput, - ChatEditorInput + ChatEditorInput, + MultiDiffEditorInput } export const enum TabModelOperationKind { @@ -769,11 +776,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 @@ -1129,6 +1141,8 @@ export interface VariablesResult { name: string; value: string; type?: string; + language?: string; + expression?: string; hasNamedChildren: boolean; indexedChildrenCount: number; extensionId: string; @@ -1182,29 +1196,28 @@ export interface ExtHostSpeechShape { $cancelKeywordRecognitionSession(session: number): Promise; } -export interface MainThreadChatProviderShape extends IDisposable { - $registerProvider(handle: number, identifier: string, metadata: IChatResponseProviderMetadata): void; +export interface MainThreadLanguageModelsShape extends IDisposable { + $registerLanguageModelProvider(handle: number, identifier: string, metadata: ILanguageModelChatMetadata): void; $unregisterProvider(handle: number): void; $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise; - $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise; + $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise; $fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; } -export interface ExtHostChatProviderShape { - $updateLanguageModels(data: { added?: string[]; removed?: string[] }): void; +export interface ExtHostLanguageModelsShape { + $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, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void; $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1231,8 +1244,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; @@ -1257,7 +1269,7 @@ export interface ExtHostChatVariablesShape { } export interface MainThreadInlineChatShape extends IDisposable { - $registerInteractiveEditorProvider(handle: number, label: string, debugName: string, supportsFeedback: boolean, supportsFollowups: boolean, supportsIssueReporting: boolean): Promise; + $registerInteractiveEditorProvider(handle: number, label: string, extensionId: ExtensionIdentifier, supportsFeedback: boolean, supportsFollowups: boolean, supportsIssueReporting: boolean): Promise; $handleProgressChunk(requestId: string, chunk: Dto): Promise; $unregisterInteractiveEditorProvider(handle: number): Promise; } @@ -1280,11 +1292,6 @@ export interface MainThreadUrlsShape extends IDisposable { export interface IChatDto { id: number; - requesterUsername: string; - requesterAvatarIconUri?: UriComponents; - responderUsername: string; - responderAvatarIconUri?: UriComponents; - inputPlaceholder?: string; } export interface IChatRequestDto { @@ -1318,7 +1325,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; } @@ -1406,6 +1412,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; @@ -1819,6 +1826,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; } @@ -2068,16 +2076,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 { @@ -2085,10 +2103,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 { @@ -2111,7 +2130,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; @@ -2130,7 +2151,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; @@ -2157,7 +2178,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; @@ -2253,6 +2274,15 @@ export interface ExtHostTerminalServiceShape { $provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise | undefined>; } +export interface ExtHostTerminalShellIntegrationShape { + $shellIntegrationChange(instanceId: number): void; + $shellExecutionStart(instanceId: number, commandLine: string | undefined, cwd: UriComponents | string | undefined): void; + $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void; + $shellExecutionData(instanceId: number, data: string): void; + $cwdChange(instanceId: number, cwd: UriComponents | string): void; + $closeTerminal(instanceId: number): void; +} + export interface ExtHostSCMShape { $provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise; $onInputBoxValueChange(sourceControlHandle: number, value: string): void; @@ -2353,14 +2383,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; } @@ -2678,13 +2708,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 */ @@ -2755,7 +2782,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. */ @@ -2773,7 +2800,7 @@ export interface MainThreadTestingShape { export const MainContext = { MainThreadAuthentication: createProxyIdentifier('MainThreadAuthentication'), MainThreadBulkEdits: createProxyIdentifier('MainThreadBulkEdits'), - MainThreadChatProvider: createProxyIdentifier('MainThreadChatProvider'), + MainThreadLanguageModels: createProxyIdentifier('MainThreadLanguageModels'), MainThreadChatAgents2: createProxyIdentifier('MainThreadChatAgents2'), MainThreadChatVariables: createProxyIdentifier('MainThreadChatVariables'), MainThreadClipboard: createProxyIdentifier('MainThreadClipboard'), @@ -2807,6 +2834,7 @@ export const MainContext = { MainThreadSpeech: createProxyIdentifier('MainThreadSpeechProvider'), MainThreadTelemetry: createProxyIdentifier('MainThreadTelemetry'), MainThreadTerminalService: createProxyIdentifier('MainThreadTerminalService'), + MainThreadTerminalShellIntegration: createProxyIdentifier('MainThreadTerminalShellIntegration'), MainThreadWebviews: createProxyIdentifier('MainThreadWebviews'), MainThreadWebviewPanels: createProxyIdentifier('MainThreadWebviewPanels'), MainThreadWebviewViews: createProxyIdentifier('MainThreadWebviewViews'), @@ -2867,6 +2895,7 @@ export const ExtHostContext = { ExtHostExtensionService: createProxyIdentifier('ExtHostExtensionService'), ExtHostLogLevelServiceShape: createProxyIdentifier('ExtHostLogLevelServiceShape'), ExtHostTerminalService: createProxyIdentifier('ExtHostTerminalService'), + ExtHostTerminalShellIntegration: createProxyIdentifier('ExtHostTerminalShellIntegration'), ExtHostSCM: createProxyIdentifier('ExtHostSCM'), ExtHostSearch: createProxyIdentifier('ExtHostSearch'), ExtHostTask: createProxyIdentifier('ExtHostTask'), @@ -2898,7 +2927,7 @@ export const ExtHostContext = { ExtHostChat: createProxyIdentifier('ExtHostChat'), ExtHostChatAgents2: createProxyIdentifier('ExtHostChatAgents'), ExtHostChatVariables: createProxyIdentifier('ExtHostChatVariables'), - ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), + ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), ExtHostSpeech: createProxyIdentifier('ExtHostSpeech'), ExtHostAiRelatedInformation: createProxyIdentifier('ExtHostAiRelatedInformation'), ExtHostAiEmbeddingVector: createProxyIdentifier('ExtHostAiEmbeddingVector'), diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 84ddbd6fa55..1c562edf76a 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/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/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index d125a8b8f79..036d64b14d2 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -58,10 +58,6 @@ export class ExtHostChat implements ExtHostChatShape { this._proxy.$transferChatSession(sessionId, newWorkspace); } - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest): void { - this._proxy.$sendRequestToProvider(providerId, message); - } - async $prepareChat(handle: number, token: CancellationToken): Promise { const entry = this._chatProvider.get(handle); if (!entry) { @@ -78,11 +74,6 @@ export class ExtHostChat implements ExtHostChatShape { return { id, - requesterUsername: session.requester?.name, - requesterAvatarIconUri: session.requester?.icon, - responderUsername: session.responder?.name, - responderAvatarIconUri: session.responder?.icon, - inputPlaceholder: session.inputPlaceholder, }; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 8166c1e7d33..80a4beaffaf 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Location } from 'vs/editor/common/languages'; import { coalesce } from 'vs/base/common/arrays'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -21,8 +22,8 @@ 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 { IChatFollowup, IChatProgress, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatContentReference, 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'; import type * as vscode from 'vscode'; @@ -115,15 +116,48 @@ class ChatAgentResponseStream { }, reference(value) { throwIfDone(this.reference); - const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.ChatResponseReferencePart.to(part); - _report(dto); + + if ('variableName' in value && !value.value) { + // The participant used this variable. Does that variable have any references to pull in? + const matchingVarData = that._request.variables.variables.find(v => v.name === value.variableName); + if (matchingVarData) { + let references: Dto[] | undefined; + if (matchingVarData.references?.length) { + references = matchingVarData.references.map(r => ({ + kind: 'reference', + reference: { variableName: value.variableName, value: r.reference as URI | Location } + } satisfies IChatContentReference)); + } else { + // Participant sent a variableName reference but the variable produced no references. Show variable reference with no value + const part = new extHostTypes.ChatResponseReferencePart(value); + const dto = typeConvert.ChatResponseReferencePart.to(part); + references = [dto]; + } + + references.forEach(r => _report(r)); + return this; + } else { + // Something went wrong- that variable doesn't actually exist + } + } else { + const part = new extHostTypes.ChatResponseReferencePart(value); + const dto = typeConvert.ChatResponseReferencePart.to(part); + _report(dto); + } + return this; }, push(part) { throwIfDone(this.push); - const dto = typeConvert.ChatResponsePart.to(part); - _report(dto); + + if (part instanceof extHostTypes.ChatResponseReferencePart) { + // Ensure variable reference values get fixed up + this.reference(part.value); + } else { + const dto = typeConvert.ChatResponsePart.to(part, that._commandsConverter, that._sessionDisposables); + _report(dto); + } + return this; }, report(progress) { @@ -166,12 +200,21 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); } - createChatAgent(extension: IExtensionDescription, name: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, name, {}); + this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); + return agent.apiAgent; + } + + createDynamicChatAgent(extension: IExtensionDescription, id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + const handle = ExtHostChatAgents2._idPool++; + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); + this._agents.set(handle, agent); + + this._proxy.$registerAgent(handle, extension.identifier, id, {}, { name, description }); return agent.apiAgent; } @@ -213,7 +256,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(); @@ -231,11 +274,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { { ...ehResult, metadata: undefined }; // REQUEST turn - res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), { extensionId: '', name: h.request.agentId })); + res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), h.request.agentId)); // RESPONSE turn const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.fromContent(r, this.commands.converter))); - res.push(new extHostTypes.ChatResponseTurn(parts, result, { extensionId: '', name: h.request.agentId }, h.request.command)); + res.push(new extHostTypes.ChatResponseTurn(parts, result, h.request.agentId, h.request.command)); } return res; @@ -245,38 +288,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,9 +380,7 @@ 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; private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined; private _isDefault: boolean | undefined; @@ -368,7 +394,7 @@ class ExtHostChatAgent { private _supportIssueReporting: boolean | undefined; 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 +420,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 +478,6 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - description: this._description ?? '', fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : @@ -485,16 +487,14 @@ 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), helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix), sampleRequest: this._sampleRequest, supportIssueReporting: this._supportIssueReporting, - isSticky: this._isSticky, + requester: this._requester }); updateScheduled = false; }); @@ -502,16 +502,9 @@ class ExtHostChatAgent { const that = this; return { - get name() { + get id() { return that.id; }, - get description() { - return that._description ?? ''; - }, - set description(v) { - that._description = v; - updateMetadataSoon(); - }, get fullName() { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); return that._fullName ?? that.extension.displayName ?? that.extension.name; @@ -535,13 +528,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 +550,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 +559,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 +568,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(); }, @@ -655,16 +629,15 @@ class ExtHostChatAgent { ? undefined! : this._onDidPerformAction.event , - get isSticky() { - return that._isSticky; - }, - set isSticky(v) { - that._isSticky = v; + 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); @@ -672,7 +645,7 @@ class ExtHostChatAgent { } satisfies vscode.ChatParticipant; } - invoke(request: vscode.ChatRequest, context: vscode.ChatContext, response: vscode.ChatExtendedResponseStream, token: CancellationToken): vscode.ProviderResult { + invoke(request: vscode.ChatRequest, context: vscode.ChatContext, response: vscode.ChatExtendedResponseStream, token: CancellationToken): vscode.ProviderResult { return this._requestHandler(request, context, response, token); } } diff --git a/src/vs/workbench/api/common/extHostChatProvider.ts b/src/vs/workbench/api/common/extHostChatProvider.ts deleted file mode 100644 index 8ad8318f9e1..00000000000 --- a/src/vs/workbench/api/common/extHostChatProvider.ts +++ /dev/null @@ -1,308 +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 { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostChatProviderShape, IMainContext, MainContext, MainThreadChatProviderShape } from 'vs/workbench/api/common/extHost.protocol'; -import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import type * as vscode from 'vscode'; -import { Progress } from 'vs/platform/progress/common/progress'; -import { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider'; -import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { AsyncIterableSource } 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'; - -type LanguageModelData = { - readonly extension: ExtensionIdentifier; - readonly provider: vscode.ChatResponseProvider; -}; - -class LanguageModelResponseStream { - - readonly stream = new AsyncIterableSource(); - - constructor( - readonly option: number, - stream?: AsyncIterableSource - ) { - this.stream = stream ?? new AsyncIterableSource(); - } -} - -class LanguageModelRequest { - - readonly apiObject: vscode.LanguageModelResponse; - - private readonly _responseStreams = new Map(); - private readonly _defaultStream = new AsyncIterableSource(); - private _isDone: boolean = false; - - constructor( - promise: Promise, - readonly cts: CancellationTokenSource - ) { - const that = this; - this.apiObject = { - result: promise, - stream: that._defaultStream.asyncIterable, - // responses: 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() { - if (this._responseStreams.size > 0) { - for (const [, value] of this._responseStreams) { - yield value.stream; - } - } else { - yield this._defaultStream; - } - } - - handleFragment(fragment: IChatResponseFragment): void { - if (this._isDone) { - return; - } - let res = this._responseStreams.get(fragment.index); - if (!res) { - if (this._responseStreams.size === 0) { - // the first response claims the default response - res = new LanguageModelResponseStream(fragment.index, this._defaultStream); - } else { - res = new LanguageModelResponseStream(fragment.index); - } - this._responseStreams.set(fragment.index, res); - } - res.stream.emitOne(fragment.part); - } - -} - -export class ExtHostChatProvider implements ExtHostChatProviderShape { - - private static _idPool = 1; - - private readonly _proxy: MainThreadChatProviderShape; - private readonly _onDidChangeModelAccess = new Emitter<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); - private readonly _onDidChangeProviders = new Emitter(); - 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 _modelAccessList = new ExtensionIdentifierMap(); - private readonly _pendingRequest = new Map(); - - - constructor( - mainContext: IMainContext, - private readonly _logService: ILogService, - private readonly _extHostAuthentication: ExtHostAuthentication, - ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadChatProvider); - } - - dispose(): void { - this._onDidChangeModelAccess.dispose(); - this._onDidChangeProviders.dispose(); - } - - registerLanguageModel(extension: IExtensionDescription, identifier: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata): IDisposable { - - const handle = ExtHostChatProvider._idPool++; - this._languageModels.set(handle, { extension: extension.identifier, provider }); - let auth; - if (metadata.auth) { - auth = { - providerLabel: extension.displayName || extension.name, - accountLabel: typeof metadata.auth === 'object' ? metadata.auth.label : undefined - }; - } - this._proxy.$registerProvider(handle, identifier, { - extension: extension.identifier, - model: metadata.name ?? '', - auth - }); - - return toDisposable(() => { - this._languageModels.delete(handle); - this._proxy.$unregisterProvider(handle); - }); - } - - async $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { - const data = this._languageModels.get(handle); - if (!data) { - return; - } - const progress = new Progress(async fragment => { - if (token.isCancellationRequested) { - this._logService.warn(`[CHAT](${data.extension.value}) CANNOT send progress because the REQUEST IS CANCELLED`); - return; - } - this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); - }); - - return data.provider.provideLanguageModelResponse2(messages.map(typeConvert.LanguageModelMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); - } - - //#region --- making request - - $updateLanguageModels(data: { added?: string[] | 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); - } - } - if (data.removed) { - for (const id of data.removed) { - // clean up - this._languageModelIds.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(); - this._pendingRequest.delete(key); - } - } - } - } - - this._onDidChangeProviders.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed) - })); - } - - getLanguageModelIds(): string[] { - return Array.from(this._languageModelIds); - } - - $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { - const updated = new Array<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); - for (const { from, to, enabled } of data) { - const set = this._modelAccessList.get(from) ?? new ExtensionIdentifierSet(); - const oldValue = set.has(to); - if (oldValue !== enabled) { - if (enabled) { - set.add(to); - } else { - set.delete(to); - } - this._modelAccessList.set(from, set); - const newItem = { from, to }; - updated.push(newItem); - this._onDidChangeModelAccess.fire(newItem); - } - } - } - - 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`); - } - - if (this._isUsingAuth(from, metadata)) { - await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, justification); - } - - 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; - }, - }; - } - - async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise { - const data = this._pendingRequest.get(requestId);//.report(chunk); - if (data) { - data.res.handleFragment(chunk); - } - } - - // BIG HACK: Using AuthenticationProviders to check access to Language Models - private async _getAuthAccess(from: IExtensionDescription, to: { identifier: ExtensionIdentifier; displayName: string }, justification?: string): 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'); - } - } - - this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); - } - - private _isUsingAuth(from: ExtensionIdentifier, toMetadata: IChatResponseProviderMetadata): toMetadata is IChatResponseProviderMetadata & { auth: NonNullable } { - // If the 'to' extension uses an auth check - return !!toMetadata.auth - // And we're asking from a different extension - && !ExtensionIdentifier.equals(toMetadata.extension, from); - } -} diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index e56e51676d4..26fac7e95e0 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -109,8 +109,13 @@ class ChatVariableResolverResponseStream { }, push(part) { throwIfDone(this.push); - const dto = typeConvert.ChatResponsePart.to(part); - _report(dto as IChatVariableResolverProgressDto); + + if (part instanceof extHostTypes.ChatResponseReferencePart) { + _report(typeConvert.ChatResponseReferencePart.to(part)); + } else if (part instanceof extHostTypes.ChatResponseProgressPart) { + _report(typeConvert.ChatResponseProgressPart.to(part)); + } + return this; } }; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 0f04b3f2455..72cf9d3de72 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/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/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 38d5f2a3205..e38d3ee11b1 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/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/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index 1bdb2bf11e4..d0e035825a6 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/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/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index 26c8795aaa6..881140392bd 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -231,7 +231,7 @@ export class ExtensionsActivator implements IDisposable { public activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { const desc = this._registry.getExtensionDescription(extensionId); if (!desc) { - throw new Error(`Extension '${extensionId}' is not known`); + throw new Error(`Extension '${extensionId.value}' is not known`); } return this._activateExtensions([{ id: desc.identifier, reason }]); } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 91f910aa4c8..2679364d945 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/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/src/vs/workbench/api/common/extHostInlineChat.ts b/src/vs/workbench/api/common/extHostInlineChat.ts index 9e8478b98f3..efd9ee8b248 100644 --- a/src/vs/workbench/api/common/extHostInlineChat.ts +++ b/src/vs/workbench/api/common/extHostInlineChat.ts @@ -96,7 +96,7 @@ export class ExtHostInteractiveEditor implements ExtHostInlineChatShape { registerProvider(extension: Readonly, provider: vscode.InteractiveEditorSessionProvider, metadata?: vscode.InteractiveEditorSessionProviderMetadata): vscode.Disposable { const wrapper = new ProviderWrapper(extension, provider); this._inputProvider.set(wrapper.handle, wrapper); - this._proxy.$registerInteractiveEditorProvider(wrapper.handle, metadata?.label ?? extension.displayName ?? extension.name, extension.identifier.value, typeof provider.handleInteractiveEditorResponseFeedback === 'function', typeof provider.provideFollowups === 'function', metadata?.supportReportIssue ?? false); + this._proxy.$registerInteractiveEditorProvider(wrapper.handle, metadata?.label ?? extension.displayName ?? extension.name, extension.identifier, typeof provider.handleInteractiveEditorResponseFeedback === 'function', typeof provider.provideFollowups === 'function', metadata?.supportReportIssue ?? false); return toDisposable(() => { this._proxy.$unregisterInteractiveEditorProvider(wrapper.handle); this._inputProvider.delete(wrapper.handle); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 1c71f5257ef..7ee4653eb4e 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/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'; @@ -541,9 +541,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, @@ -574,9 +572,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); @@ -586,20 +584,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); } } @@ -1234,7 +1252,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 { @@ -1349,11 +1367,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)); } } } @@ -2019,10 +2038,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, @@ -2031,25 +2046,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, - }; + })); } } @@ -2573,9 +2588,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); } @@ -2798,13 +2813,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); } @@ -2827,10 +2841,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, }); @@ -2841,8 +2856,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/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts new file mode 100644 index 00000000000..8221534013c --- /dev/null +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -0,0 +1,384 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +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, Barrier } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { localize } from 'vs/nls'; +import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +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; +}; + +class LanguageModelResponseStream { + + readonly stream = new AsyncIterableSource(); + + constructor( + readonly option: number, + stream?: AsyncIterableSource + ) { + this.stream = stream ?? new AsyncIterableSource(); + } +} + +class LanguageModelResponse { + + readonly apiObject: vscode.LanguageModelChatResponse; + + private readonly _responseStreams = new Map(); + private readonly _defaultStream = new AsyncIterableSource(); + private _isDone: boolean = false; + private _isStreaming: boolean = false; + + constructor() { + + const that = this; + this.apiObject = { + // result: promise, + stream: that._defaultStream.asyncIterable, + // streams: AsyncIterable[] // FUTURE responses per N + }; + } + + private * _streams() { + if (this._responseStreams.size > 0) { + for (const [, value] of this._responseStreams) { + yield value.stream; + } + } else { + yield this._defaultStream; + } + } + + handleFragment(fragment: IChatResponseFragment): void { + if (this._isDone) { + return; + } + this._isStreaming = true; + let res = this._responseStreams.get(fragment.index); + if (!res) { + if (this._responseStreams.size === 0) { + // the first response claims the default response + res = new LanguageModelResponseStream(fragment.index, this._defaultStream); + } else { + res = new LanguageModelResponseStream(fragment.index); + } + this._responseStreams.set(fragment.index, res); + } + 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; + private readonly _onDidChangeModelAccess = new Emitter<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); + private readonly _onDidChangeProviders = new Emitter(); + readonly onDidChangeProviders = this._onDidChangeProviders.event; + + private readonly _languageModels = new Map(); + 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(); + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @ILogService private readonly _logService: ILogService, + @IExtHostAuthentication private readonly _extHostAuthentication: IExtHostAuthentication, + ) { + this._proxy = extHostRpc.getProxy(MainContext.MainThreadLanguageModels); + } + + dispose(): void { + this._onDidChangeModelAccess.dispose(); + this._onDidChangeProviders.dispose(); + } + + registerLanguageModel(extension: IExtensionDescription, identifier: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata): IDisposable { + + const handle = ExtHostLanguageModels._idPool++; + this._languageModels.set(handle, { extension: extension.identifier, provider, languageModelId: identifier }); + let auth; + if (metadata.auth) { + auth = { + providerLabel: extension.displayName || extension.name, + accountLabel: typeof metadata.auth === 'object' ? metadata.auth.label : undefined + }; + } + this._proxy.$registerLanguageModelProvider(handle, identifier, { + extension: extension.identifier, + identifier: identifier, + model: metadata.name ?? '', + auth + }); + + return toDisposable(() => { + this._languageModels.delete(handle); + this._proxy.$unregisterProvider(handle); + }); + } + + async $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { + const data = this._languageModels.get(handle); + if (!data) { + return; + } + const progress = new Progress(async fragment => { + if (token.isCancellationRequested) { + this._logService.warn(`[CHAT](${data.extension.value}) CANNOT send progress because the REQUEST IS CANCELLED`); + return; + } + this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); + }); + + return data.provider.provideLanguageModelResponse2(messages.map(typeConvert.LanguageModelMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); + } + + //#region --- making request + + $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[] | undefined; removed?: string[] | undefined }): void { + const added: string[] = []; + const removed: string[] = []; + if (data.added) { + 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._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.reject(new CancellationError()); + this._pendingRequest.delete(key); + } + } + } + } + + this._onDidChangeProviders.fire(Object.freeze({ + 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._allLanguageModelData.keys()); + } + + $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { + const updated = new Array<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); + for (const { from, to, enabled } of data) { + const set = this._modelAccessList.get(from) ?? new ExtensionIdentifierSet(); + const oldValue = set.has(to); + if (oldValue !== enabled) { + if (enabled) { + set.add(to); + } else { + set.delete(to); + } + this._modelAccessList.set(from, set); + const newItem = { from, to }; + updated.push(newItem); + this._onDidChangeModelAccess.fire(newItem); + } + } + } + + async sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { + + const from = extension.identifier; + const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, options.justification); + + if (!metadata || !this._allLanguageModelData.has(languageModelId)) { + throw LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); + } + + if (this._isUsingAuth(from, metadata)) { + const success = await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, options.justification, options.silent); + + if (!success || !this._modelAccessList.get(from)?.has(metadata.extension)) { + throw LanguageModelError.NoPermissions(`Language model '${languageModelId}' cannot be used by '${from.value}'.`); + } + } + + const requestId = (Math.random() * 1e6) | 0; + 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 }); + + 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); + res.resolve(); + barrier.open(); + }); + + await barrier.wait(); + + if (error) { + throw new LanguageModelError( + `Language model '${languageModelId}' errored, check cause for more details`, + 'Unknown', + error + ); + } + + return res.apiObject; + } + + async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise { + const data = this._pendingRequest.get(requestId);//.report(chunk); + if (data) { + data.res.handleFragment(chunk); + } + } + + // BIG HACK: Using AuthenticationProviders to check access to Language Models + 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) { + this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); + return true; + } + + if (silent) { + return false; + } + + 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 } { + // If the 'to' extension uses an auth check + return !!toMetadata.auth + // 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/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 2cc7a200edc..8f74a0a4b69 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -442,17 +442,7 @@ export class ExtHostNotebookDocument { return this._cells[index]; } - getCell(cellHandle: number | URI): ExtHostCell | undefined { - if (URI.isUri(cellHandle)) { - const data = notebookCommon.CellUri.parse(cellHandle); - if (!data) { - return undefined; - } - if (data.notebook.toString() !== this.uri.toString()) { - return undefined; - } - cellHandle = data.handle; - } + getCell(cellHandle: number): ExtHostCell | undefined { return this._cells.find(cell => cell.handle === cellHandle); } diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index f2a201e9e06..49dcbf40085 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/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,9 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { name: result.variable.name, value: result.variable.value, type: result.variable.type, + interfaces: result.variable.interfaces, + language: result.variable.language, + expression: result.variable.expression, hasNamedChildren: result.hasNamedChildren, indexedChildrenCount: result.indexedChildrenCount, extensionId: obj.extensionId.value, @@ -700,7 +708,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 +720,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/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index d0289669abf..c2e0b93f7b9 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/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/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index e004acba809..fb4f5520c02 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -54,6 +54,8 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID registerProfileProvider(extension: IExtensionDescription, id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable; registerTerminalQuickFixProvider(id: string, extensionId: string, provider: vscode.TerminalQuickFixProvider): vscode.Disposable; getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection; + getTerminalById(id: number): ExtHostTerminal | null; + getTerminalIdByApiObject(apiTerminal: vscode.Terminal): number | null; } interface IEnvironmentVariableCollection extends vscode.EnvironmentVariableCollection { @@ -63,6 +65,7 @@ interface IEnvironmentVariableCollection extends vscode.EnvironmentVariableColle export interface ITerminalInternalOptions { cwd?: string | URI; isFeatureTerminal?: boolean; + forceShellIntegration?: boolean; useShellEnvironment?: boolean; resolvedExtHostIdentifier?: ExtHostTerminalIdentifier; /** @@ -74,7 +77,7 @@ export interface ITerminalInternalOptions { export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); -export class ExtHostTerminal { +export class ExtHostTerminal extends Disposable { private _disposed: boolean = false; private _pidPromise: Promise; private _cols: number | undefined; @@ -84,16 +87,23 @@ export class ExtHostTerminal { private _state: vscode.TerminalState = { isInteractedWith: false }; private _selection: string | undefined; + shellIntegration: vscode.TerminalShellIntegration | undefined; + public isOpen: boolean = false; readonly value: vscode.Terminal; + protected readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + constructor( private _proxy: MainThreadTerminalServiceShape, public _id: ExtHostTerminalIdentifier, private readonly _creationOptions: vscode.TerminalOptions | vscode.ExtensionTerminalOptions, private _name?: string, ) { + super(); + this._creationOptions = Object.freeze(this._creationOptions); this._pidPromise = new Promise(c => this._pidPromiseComplete = c); @@ -117,6 +127,9 @@ export class ExtHostTerminal { get selection(): string | undefined { return that._selection; }, + get shellIntegration(): vscode.TerminalShellIntegration | undefined { + return that.shellIntegration; + }, sendText(text: string, shouldExecute: boolean = true): void { that._checkDisposed(); that._proxy.$sendText(that._id, text, shouldExecute); @@ -147,6 +160,11 @@ export class ExtHostTerminal { }; } + override dispose(): void { + this._onWillDispose.fire(); + super.dispose(); + } + public async create( options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions, @@ -165,6 +183,7 @@ export class ExtHostTerminal { initialText: options.message ?? undefined, strictEnv: options.strictEnv ?? undefined, hideFromUser: options.hideFromUser ?? undefined, + forceShellIntegration: internalOptions?.forceShellIntegration ?? undefined, isFeatureTerminal: internalOptions?.isFeatureTerminal ?? undefined, isExtensionOwnedTerminal: true, useShellEnvironment: internalOptions?.useShellEnvironment ?? undefined, @@ -423,7 +442,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I processArgument: arg => { const deserialize = (arg: any) => { const cast = arg as ISerializedTerminalInstanceContext; - return this._getTerminalById(cast.instanceId)?.value; + return this.getTerminalById(cast.instanceId)?.value; }; switch (arg?.$mid) { case MarshalledId.TerminalContext: return deserialize(arg); @@ -496,7 +515,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void { - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (!terminal) { throw new Error(`Cannot resolve terminal with id ${id} for virtual process`); } @@ -514,7 +533,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } return; } - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (terminal) { this._activeTerminal = terminal; if (original !== this._activeTerminal) { @@ -524,14 +543,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptTerminalProcessData(id: number, data: string): Promise { - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (terminal) { this._onDidWriteTerminalData.fire({ terminal: terminal.value, data }); } } public async $acceptTerminalDimensions(id: number, cols: number, rows: number): Promise { - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (terminal) { if (terminal.setDimensions(cols, rows)) { this._onDidChangeTerminalDimensions.fire({ @@ -543,7 +562,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptDidExecuteCommand(id: number, command: ITerminalCommandDto): Promise { - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (terminal) { this._onDidExecuteCommand.fire({ terminal: terminal.value, ...command }); } @@ -556,7 +575,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptTerminalTitleChange(id: number, name: string): Promise { - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (terminal) { terminal.name = name; } @@ -599,14 +618,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptTerminalProcessId(id: number, processId: number): Promise { - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); terminal?._setProcessId(processId); } public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise { // Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call // Pseudoterminal.start - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (!terminal) { return { message: localize('launchFail.idMissingOnExtHost', "Could not find the terminal with id {0} on the extension host", id) }; } @@ -663,14 +682,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public $acceptTerminalInteraction(id: number): void { - const terminal = this._getTerminalById(id); + const terminal = this.getTerminalById(id); if (terminal?.setInteractedWith()) { this._onDidChangeTerminalState.fire(terminal.value); } } public $acceptTerminalSelection(id: number, selection: string | undefined): void { - this._getTerminalById(id)?.setSelection(selection); + this.getTerminalById(id)?.setSelection(selection); } public $acceptProcessResize(id: number, cols: number, rows: number): void { @@ -793,7 +812,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $provideLinks(terminalId: number, line: string): Promise { - const terminal = this._getTerminalById(terminalId); + const terminal = this.getTerminalById(terminalId); if (!terminal) { return []; } @@ -876,10 +895,17 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I this._proxy.$sendProcessExit(id, exitCode); } - private _getTerminalById(id: number): ExtHostTerminal | null { + public getTerminalById(id: number): ExtHostTerminal | null { return this._getTerminalObjectById(this._terminals, id); } + public getTerminalIdByApiObject(terminal: vscode.Terminal): number | null { + const index = this._terminals.findIndex(item => { + return item.value === terminal; + }); + return index >= 0 ? index : null; + } + private _getTerminalObjectById(array: T[], id: number): T | null { const index = this._getTerminalObjectIndexById(array, id); return index !== null ? array[index] : null; @@ -889,10 +915,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I const index = array.findIndex(item => { return item._id === id; }); - if (index === -1) { - return null; - } - return index; + return index >= 0 ? index : null; } public getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection { diff --git a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts new file mode 100644 index 00000000000..0e2a6358f13 --- /dev/null +++ b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; +import { Emitter, type Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { AsyncIterableObject, Barrier, type AsyncIterableEmitter } from 'vs/base/common/async'; + +export interface IExtHostTerminalShellIntegration extends ExtHostTerminalShellIntegrationShape { + readonly _serviceBrand: undefined; + + readonly onDidChangeTerminalShellIntegration: Event; + readonly onDidStartTerminalShellExecution: Event; + readonly onDidEndTerminalShellExecution: Event; +} +export const IExtHostTerminalShellIntegration = createDecorator('IExtHostTerminalShellIntegration'); + +export class ExtHostTerminalShellIntegration extends Disposable implements IExtHostTerminalShellIntegration { + + readonly _serviceBrand: undefined; + + protected _proxy: MainThreadTerminalShellIntegrationShape; + + private _activeShellIntegrations: Map = new Map(); + + protected readonly _onDidChangeTerminalShellIntegration = new Emitter(); + readonly onDidChangeTerminalShellIntegration = this._onDidChangeTerminalShellIntegration.event; + protected readonly _onDidStartTerminalShellExecution = new Emitter(); + readonly onDidStartTerminalShellExecution = this._onDidStartTerminalShellExecution.event; + protected readonly _onDidEndTerminalShellExecution = new Emitter(); + readonly onDidEndTerminalShellExecution = this._onDidEndTerminalShellExecution.event; + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @IExtHostTerminalService private readonly _extHostTerminalService: IExtHostTerminalService, + ) { + super(); + + this._proxy = extHostRpc.getProxy(MainContext.MainThreadTerminalShellIntegration); + + // Clean up listeners + this._register(toDisposable(() => { + for (const [_, integration] of this._activeShellIntegrations) { + integration.dispose(); + } + this._activeShellIntegrations.clear(); + })); + + // Convenient test code: + // this.onDidChangeTerminalShellIntegration(e => { + // console.log('*** onDidChangeTerminalShellIntegration', e); + // }); + // this.onDidStartTerminalShellExecution(async e => { + // console.log('*** onDidStartTerminalShellExecution', e); + // // new Promise(r => { + // // (async () => { + // // for await (const d of e.createDataStream()) { + // // console.log('data2', d); + // // } + // // })(); + // // }); + // for await (const d of e.createDataStream()) { + // console.log('data', d); + // } + // }); + // this.onDidEndTerminalShellExecution(e => { + // console.log('*** onDidEndTerminalShellExecution', e); + // }); + // setTimeout(() => { + // Array.from(this._activeShellIntegrations.values())[0].value.executeCommand('echo hello'); + // }, 4000); + } + + public $shellIntegrationChange(instanceId: number): void { + const terminal = this._extHostTerminalService.getTerminalById(instanceId); + if (!terminal) { + return; + } + + const apiTerminal = terminal.value; + let shellIntegration = this._activeShellIntegrations.get(instanceId); + if (!shellIntegration) { + shellIntegration = new InternalTerminalShellIntegration(terminal.value, this._onDidStartTerminalShellExecution); + this._activeShellIntegrations.set(instanceId, shellIntegration); + shellIntegration.store.add(terminal.onWillDispose(() => this._activeShellIntegrations.get(instanceId)?.dispose())); + shellIntegration.store.add(shellIntegration.onDidRequestShellExecution(commandLine => this._proxy.$executeCommand(instanceId, commandLine))); + shellIntegration.store.add(shellIntegration.onDidRequestEndExecution(e => this._onDidEndTerminalShellExecution.fire(e.value))); + shellIntegration.store.add(shellIntegration.onDidRequestChangeShellIntegration(e => this._onDidChangeTerminalShellIntegration.fire(e))); + terminal.shellIntegration = shellIntegration.value; + } + this._onDidChangeTerminalShellIntegration.fire({ + terminal: apiTerminal, + shellIntegration: shellIntegration.value + }); + } + + public $shellExecutionStart(instanceId: number, commandLine: string, cwd: URI | string | undefined): void { + // Force shellIntegration creation if it hasn't been created yet, this could when events + // don't come through on startup + if (!this._activeShellIntegrations.has(instanceId)) { + this.$shellIntegrationChange(instanceId); + } + this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, cwd); + } + + public $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void { + this._activeShellIntegrations.get(instanceId)?.endShellExecution(commandLine, exitCode); + } + + public $shellExecutionData(instanceId: number, data: string): void { + this._activeShellIntegrations.get(instanceId)?.emitData(data); + } + + public $cwdChange(instanceId: number, cwd: string): void { + this._activeShellIntegrations.get(instanceId)?.setCwd(cwd); + } + + public $closeTerminal(instanceId: number): void { + this._activeShellIntegrations.get(instanceId)?.dispose(); + this._activeShellIntegrations.delete(instanceId); + + } +} + +class InternalTerminalShellIntegration extends Disposable { + private _currentExecution: InternalTerminalShellExecution | undefined; + get currentExecution(): InternalTerminalShellExecution | undefined { return this._currentExecution; } + + private _ignoreNextExecution: boolean = false; + private _cwd: URI | string | undefined; + + readonly store: DisposableStore = this._register(new DisposableStore()); + + readonly value: vscode.TerminalShellIntegration; + + protected readonly _onDidRequestChangeShellIntegration = this._register(new Emitter()); + readonly onDidRequestChangeShellIntegration = this._onDidRequestChangeShellIntegration.event; + protected readonly _onDidRequestShellExecution = this._register(new Emitter()); + readonly onDidRequestShellExecution = this._onDidRequestShellExecution.event; + protected readonly _onDidRequestEndExecution = this._register(new Emitter()); + readonly onDidRequestEndExecution = this._onDidRequestEndExecution.event; + + constructor( + private readonly _terminal: vscode.Terminal, + private readonly _onDidStartTerminalShellExecution: Emitter + ) { + super(); + + const that = this; + this.value = { + get cwd(): URI | string | undefined { + return that._cwd; + }, + executeCommand(commandLine): vscode.TerminalShellExecution { + that._onDidRequestShellExecution.fire(commandLine); + const execution = that.startShellExecution(commandLine, that._cwd).value; + that._ignoreNextExecution = true; + return execution; + } + }; + } + + startShellExecution(commandLine: string, cwd: URI | string | undefined): InternalTerminalShellExecution { + if (this._ignoreNextExecution && this._currentExecution) { + this._ignoreNextExecution = false; + } else { + if (this._currentExecution) { + this._currentExecution.endExecution(undefined, undefined); + this._onDidRequestEndExecution.fire(this._currentExecution); + } + this._currentExecution = new InternalTerminalShellExecution(this._terminal, commandLine, cwd); + this._onDidStartTerminalShellExecution.fire(this._currentExecution.value); + } + return this._currentExecution; + } + + emitData(data: string): void { + this.currentExecution?.emitData(data); + } + + endShellExecution(commandLine: string | undefined, exitCode: number | undefined): void { + if (this._currentExecution) { + this._currentExecution.endExecution(commandLine, exitCode); + this._onDidRequestEndExecution.fire(this._currentExecution); + this._currentExecution = undefined; + } + } + + setCwd(cwd: URI | string): void { + let wasChanged = false; + if (URI.isUri(this._cwd)) { + if (this._cwd.toString() !== cwd.toString()) { + wasChanged = true; + } + } else if (this._cwd !== cwd) { + wasChanged = true; + } + if (wasChanged) { + this._cwd = cwd; + this._onDidRequestChangeShellIntegration.fire({ terminal: this._terminal, shellIntegration: this.value }); + } + } +} + +class InternalTerminalShellExecution { + private _dataStream: ShellExecutionDataStream | undefined; + + private readonly _exitCode: Promise; + private _exitCodeResolve: ((exitCode: number | undefined) => void) | undefined; + + readonly value: vscode.TerminalShellExecution; + + constructor( + readonly terminal: vscode.Terminal, + private _commandLine: string | undefined, + readonly cwd: URI | string | undefined, + ) { + this._exitCode = new Promise(resolve => { + this._exitCodeResolve = resolve; + }); + + const that = this; + this.value = { + get terminal(): vscode.Terminal { + return terminal; + }, + get commandLine(): string | undefined { + return that._commandLine; + }, + get cwd(): URI | string | undefined { + return cwd; + }, + get exitCode(): Promise { + return that._exitCode; + }, + createDataStream(): AsyncIterable { + return that._createDataStream(); + } + }; + } + + private _createDataStream(): AsyncIterable { + if (!this._dataStream) { + if (this._exitCodeResolve === undefined) { + return AsyncIterableObject.EMPTY; + } + this._dataStream = new ShellExecutionDataStream(); + } + return this._dataStream.createIterable(); + } + + emitData(data: string): void { + this._dataStream?.emitData(data); + } + + endExecution(commandLine: string | undefined, exitCode: number | undefined): void { + if (commandLine) { + this._commandLine = commandLine; + } + this._dataStream?.endExecution(); + this._dataStream = undefined; + this._exitCodeResolve?.(exitCode); + this._exitCodeResolve = undefined; + } +} + +class ShellExecutionDataStream extends Disposable { + private _barrier: Barrier | undefined; + private _emitters: AsyncIterableEmitter[] = []; + + createIterable(): AsyncIterable { + const barrier = this._barrier = new Barrier(); + const iterable = new AsyncIterableObject(async emitter => { + this._emitters.push(emitter); + await barrier.wait(); + }); + return iterable; + } + + emitData(data: string): void { + for (const emitter of this._emitters) { + emitter.emitOne(data); + } + } + + endExecution(): void { + this._barrier?.open(); + this._barrier = undefined; + } +} diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 597865e61c0..3cd53e11d89 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/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,8 +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 { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +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 type * as vscode from 'vscode'; interface ControllerInfo { @@ -74,9 +72,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), }; } @@ -154,7 +154,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return new TestItemImpl(controllerId, id, label, uri); }, createTestRun: (request, name, persist = true) => { - return this.runTracker.createTestRun(extension, controllerId, collection, request, name, persist); + return this.runTracker.createTestRun(controllerId, collection, request, name, persist); }, invalidateTestResults: items => { if (items === undefined) { @@ -235,19 +235,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 +291,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), @@ -356,7 +353,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return {}; } - const { collection, profiles, extension } = lookup; + const { collection, profiles } = lookup; const profile = profiles.get(req.profileId); if (!profile) { return {}; @@ -387,7 +384,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { const tracker = isStartControllerTests(req) && this.runTracker.prepareForMainThreadTestRun( publicReq, TestRunDto.fromInternal(req, lookup.collection), - extension, + profile, token, ); @@ -401,8 +398,6 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { if (tracker.hasRunningTasks && !token.isCancellationRequested) { await Event.toPromise(tracker.onEnd); } - - tracker.dispose(); } } } @@ -433,16 +428,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 +448,7 @@ class TestRunTracker extends Disposable { * Gets whether there are any tests running. */ public get hasRunningTasks() { - return this.tasks.size > 0; + return this.running > 0; } /** @@ -466,8 +461,8 @@ class TestRunTracker extends Disposable { constructor( private readonly dto: TestRunDto, private readonly proxy: MainThreadTestingShape, - private readonly extension: IRelaxedExtensionDescription, private readonly logService: ILogService, + private readonly profile: vscode.TestRunProfile | undefined, parentToken?: CancellationToken, ) { super(); @@ -475,6 +470,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 +489,31 @@ 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,19 +545,42 @@ 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) { - checkProposedApiEnabled(extension, 'testCoverage'); - coverage = provider && new TestRunCoverageBearer(provider); - coverageEmitter.fire({ taskId, runId, coverage }); + // todo@connor4312: back compat + set coverageProvider(provider: ICoverageProvider | undefined) { + 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, //#region state mutation enqueued: guardTestMutation(test => { this.proxy.$updateTestStateInRun(runId, taskId, TestId.fromExtHostTestItem(test, ctrlId).toString(), TestResultState.Queued); @@ -589,13 +631,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 +693,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 +713,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 +737,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, profile: vscode.TestRunProfile, token: CancellationToken) { + return this.getTracker(req, dto, 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,11 +757,10 @@ export class TestRunCoordinator { } } - /** * Implements the public `createTestRun` API. */ - public createTestRun(extension: IRelaxedExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -736,37 +780,18 @@ export class TestRunCoordinator { persist }); - const tracker = this.getTracker(request, dto, extension); + const tracker = this.getTracker(request, dto, 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, profile: vscode.TestRunProfile | undefined, token?: CancellationToken) { + const tracker = new TestRunTracker(dto, this.proxy, 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 +864,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/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index ccf5700c83b..650b852e1e4 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/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/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index db879caea6b..caf7fcd3607 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/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,17 +37,18 @@ 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/chatProvider'; +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 { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestTag, TestMessageType, TestResultItem, denamespaceTestTag, namespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { CoverageDetails, DetailType, ICoverageCount, 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'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; @@ -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,36 +1973,49 @@ 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), }; } } export namespace TestCoverage { - function fromCoveredCount(count: vscode.CoveredCount): ICoveredCount { + function fromCoverageCount(count: vscode.TestCoverageCount): ICoverageCount { return { covered: count.covered, total: count.total }; } @@ -2012,7 +2023,11 @@ 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 (typeof coverage.executed === 'number' && coverage.executed < 0) { + throw new Error(`Invalid coverage count ${coverage.executed}`); + } + if ('branches' in coverage) { return { count: coverage.executed, @@ -2032,13 +2047,17 @@ export namespace TestCoverage { } } - export function fromFile(coverage: vscode.FileCoverage): IFileCoverage.Serialized { + export function fromFile(id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { + types.validateTestCoverageCount(coverage.statementCoverage); + types.validateTestCoverageCount(coverage.branchCoverage); + types.validateTestCoverageCount(coverage.declarationCoverage); + 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), + statement: fromCoverageCount(coverage.statementCoverage), + branch: coverage.branchCoverage && fromCoverageCount(coverage.branchCoverage), + declaration: coverage.declarationCoverage && fromCoverageCount(coverage.declarationCoverage), }; } } @@ -2239,20 +2258,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'); @@ -2428,22 +2447,45 @@ export namespace ChatResponseCommandButtonPart { export namespace ChatResponseReferencePart { export function to(part: vscode.ChatResponseReferencePart): Dto { + if ('variableName' in part.value) { + return { + kind: 'reference', + reference: { + variableName: part.value.variableName, + value: URI.isUri(part.value.value) || !part.value.value ? + part.value.value : + Location.from(part.value.value as vscode.Location) + } + }; + } + return { kind: 'reference', - reference: !URI.isUri(part.value) ? Location.from(part.value) : part.value + reference: URI.isUri(part.value) ? + part.value : + Location.from(part.value) }; } export function from(part: Dto): vscode.ChatResponseReferencePart { const value = revive(part); + + const mapValue = (value: URI | languages.Location): vscode.Uri | vscode.Location => URI.isUri(value) ? + value : + Location.to(value); + return new types.ChatResponseReferencePart( - URI.isUri(value.reference) ? value.reference : Location.to(value.reference) + 'variableName' in value.reference ? { + variableName: value.reference.variableName, + value: value.reference.value && mapValue(value.reference.value) + } : + mapValue(value.reference) ); } } export namespace ChatResponsePart { - export function to(part: vscode.ChatResponsePart): extHostProtocol.IChatProgressDto { + export function to(part: vscode.ChatResponsePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.to(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2454,6 +2496,8 @@ export namespace ChatResponsePart { return ChatResponseProgressPart.to(part); } else if (part instanceof types.ChatResponseFileTreePart) { return ChatResponseFilesPart.to(part); + } else if (part instanceof types.ChatResponseCommandButtonPart) { + return ChatResponseCommandButtonPart.to(part, commandsConverter, commandDisposables); } return { kind: 'content', @@ -2535,7 +2579,7 @@ export namespace ChatResponseProgress { }; } else if ('participant' in progress) { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return { agentName: progress.participant, command: progress.command, kind: 'agentDetection' }; + return { agentId: progress.participant, command: progress.command, kind: 'agentDetection' }; } else if ('message' in progress) { return { content: MarkdownString.from(progress.message), kind: 'progressMessage' }; } else { @@ -2543,36 +2587,6 @@ export namespace ChatResponseProgress { } } - export function to(progress: extHostProtocol.IChatProgressDto): vscode.ChatProgress | undefined { - switch (progress.kind) { - case 'markdownContent': - case 'inlineReference': - case 'treeData': - return ChatResponseProgress.to(progress); - case 'content': - return { content: progress.content }; - case 'usedContext': - return { documents: progress.documents.map(d => ({ uri: URI.revive(d.uri), version: d.version, ranges: d.ranges.map(r => Range.to(r)) })) }; - case 'reference': - return { - reference: - isUriComponents(progress.reference) ? - URI.revive(progress.reference) : - Location.to(progress.reference) - }; - case 'agentDetection': - // For simplicity, don't sent back the 'extended' types - return undefined; - case 'progressMessage': - return { message: progress.content.value }; - case 'vulnerability': - return { content: progress.content, vulnerabilities: progress.vulnerabilities }; - default: - // Unknown type, eg something in history that was removed? Ignore - return undefined; - } - } - export function toProgressContent(progress: extHostProtocol.IChatContentProgressDto, commandsConverter: Command.ICommandsConverter): vscode.ChatContentProgress | undefined { switch (progress.kind) { case 'markdownContent': @@ -2603,16 +2617,28 @@ 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; + case ChatAgentLocation.Editor: return types.ChatLocation.Editor; + } + } +} + 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 +2698,29 @@ export namespace TerminalQuickFix { } } +export namespace PartialAcceptInfo { + export function to(info: languages.PartialAcceptInfo): types.PartialAcceptInfo { + return { + kind: PartialAcceptTriggerKind.to(info.kind), + }; + } +} + +export namespace PartialAcceptTriggerKind { + export function to(kind: languages.PartialAcceptTriggerKind): types.PartialAcceptTriggerKind { + switch (kind) { + case languages.PartialAcceptTriggerKind.Word: + return types.PartialAcceptTriggerKind.Word; + case languages.PartialAcceptTriggerKind.Line: + return types.PartialAcceptTriggerKind.Line; + case languages.PartialAcceptTriggerKind.Suggest: + return types.PartialAcceptTriggerKind.Suggest; + default: + return types.PartialAcceptTriggerKind.Unknown; + } + } +} + export namespace DebugTreeItem { export function from(item: vscode.DebugTreeItem, id: number): IDebugVisualizationTreeItem { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 366ae1172f7..da2894fe681 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1795,6 +1795,17 @@ export class InlineSuggestionList implements vscode.InlineCompletionList { } } +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, +} + export enum ViewColumn { Active = -1, Beside = -2, @@ -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 @@ -3976,22 +4025,31 @@ export class TestTag implements vscode.TestTag { //#endregion //#region Test Coverage -export class CoveredCount implements vscode.CoveredCount { +export class TestCoverageCount implements vscode.TestCoverageCount { constructor(public covered: number, public total: number) { + validateTestCoverageCount(this); } } -const validateCC = (cc?: vscode.CoveredCount) => { - if (cc && cc.covered > cc.total) { +export function validateTestCoverageCount(cc?: vscode.TestCoverageCount) { + if (!cc) { + return; + } + + if (cc.covered > cc.total) { throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total (${cc.total})`); } -}; + + if (cc.total < 0) { + throw new Error(`The number of covered items (${cc.total}) cannot be negative`); + } +} export class FileCoverage implements vscode.FileCoverage { - public static fromDetails(uri: vscode.Uri, details: vscode.DetailedCoverage[]): vscode.FileCoverage { - const statements = new CoveredCount(0, 0); - const branches = new CoveredCount(0, 0); - const decl = new CoveredCount(0, 0); + public static fromDetails(uri: vscode.Uri, details: vscode.FileCoverageDetail[]): vscode.FileCoverage { + const statements = new TestCoverageCount(0, 0); + const branches = new TestCoverageCount(0, 0); + const decl = new TestCoverageCount(0, 0); for (const detail of details) { if ('branches' in detail) { @@ -4020,17 +4078,14 @@ export class FileCoverage implements vscode.FileCoverage { return coverage; } - detailedCoverage?: vscode.DetailedCoverage[]; + detailedCoverage?: vscode.FileCoverageDetail[]; constructor( public readonly uri: vscode.Uri, - public statementCoverage: vscode.CoveredCount, - public branchCoverage?: vscode.CoveredCount, - public declarationCoverage?: vscode.CoveredCount, + public statementCoverage: vscode.TestCoverageCount, + public branchCoverage?: vscode.TestCoverageCount, + public declarationCoverage?: vscode.TestCoverageCount, ) { - validateCC(statementCoverage); - validateCC(branchCoverage); - validateCC(declarationCoverage); } } @@ -4155,6 +4210,10 @@ export class InteractiveWindowInput { export class ChatEditorTabInput { constructor(readonly providerId: string) { } } + +export class TextMultiDiffTabInput { + constructor(readonly textDiffs: TextDiffTabInput[]) { } +} //#endregion // --- Start Positron --- @@ -4353,8 +4412,8 @@ export class ChatResponseCommandButtonPart { } export class ChatResponseReferencePart { - value: vscode.Uri | vscode.Location; - constructor(value: vscode.Uri | vscode.Location) { + value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }; + constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }) { this.value = value; } } @@ -4365,7 +4424,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn { readonly prompt: string, readonly command: string | undefined, readonly variables: vscode.ChatResolvedVariable[], - readonly participant: { extensionId: string; name: string }, + readonly participant: string, ) { } } @@ -4374,12 +4433,19 @@ export class ChatResponseTurn implements vscode.ChatResponseTurn { constructor( readonly response: ReadonlyArray, readonly result: vscode.ChatResult, - readonly participant: { extensionId: string; name: string }, + readonly participant: string, readonly command?: string ) { } } -export class LanguageModelSystemMessage { +export enum ChatLocation { + Panel = 1, + Terminal = 2, + Notebook = 3, + Editor = 4, +} + +export class LanguageModelChatSystemMessage { content: string; constructor(content: string) { @@ -4387,7 +4453,7 @@ export class LanguageModelSystemMessage { } } -export class LanguageModelUserMessage { +export class LanguageModelChatUserMessage { content: string; name: string | undefined; @@ -4397,12 +4463,34 @@ export class LanguageModelUserMessage { } } -export class LanguageModelAssistantMessage { +export class LanguageModelChatAssistantMessage { content: string; + name?: string; - constructor(content: string) { + constructor(content: string, name?: string) { this.content = content; + this.name = name; + } +} + +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 diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 1ccfd2101da..995d4f8a17b 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/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,13 +103,17 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { cwd: args.cwd, name: terminalName, iconPath: new ThemeIcon('debug'), + env: args.env, }; giveShellTimeToInitialize = true; terminal = this._terminalService.createTerminalFromOptions(options, { isFeatureTerminal: true, + // Since debug termnials are REPLs, we want shell integration to be enabled. + // Ignore isFeatureTerminal when evaluating shell integration enablement. + forceShellIntegration: true, useShellEnvironment: true }); - this._integratedTerminalInstances.insert(terminal, shellConfig); + this._integratedTerminalInstances.insert(terminal, termKey); } else { cwdForPrepareCommand = args.cwd; @@ -122,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) { @@ -136,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 @@ -156,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 { @@ -182,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 => { @@ -202,6 +219,9 @@ class DebugTerminalCollection { } if (termInfo.config !== config) { + if (cleanupOthersByName) { + terminal.dispose(); + } return null; } diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index 5e4e8ed1510..dd77886bbf0 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -19,7 +19,7 @@ import { ExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.pro import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { IActivityService } from 'vs/workbench/services/activity/common/activity'; import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationExtensionsService, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; @@ -28,6 +28,9 @@ import { TestActivityService, TestExtensionService, TestProductService, TestStor import type { AuthenticationProvider, AuthenticationSession } from 'vscode'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; +import { AuthenticationAccessService, IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationUsageService, IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationExtensionsService } from 'vs/workbench/services/authentication/browser/authenticationExtensionsService'; class AuthQuickPick { private listener: ((e: IQuickPickDidAcceptEvent) => any) | undefined; @@ -113,9 +116,12 @@ suite('ExtHostAuthentication', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService); instantiationService.stub(IProductService, TestProductService); + instantiationService.stub(IAuthenticationAccessService, instantiationService.createInstance(AuthenticationAccessService)); + instantiationService.stub(IAuthenticationUsageService, instantiationService.createInstance(AuthenticationUsageService)); const rpcProtocol = new TestRPCProtocol(); instantiationService.stub(IAuthenticationService, instantiationService.createInstance(AuthenticationService)); + instantiationService.stub(IAuthenticationExtensionsService, instantiationService.createInstance(AuthenticationExtensionsService)); rpcProtocol.set(MainContext.MainThreadAuthentication, instantiationService.createInstance(MainThreadAuthentication, rpcProtocol)); extHostAuthentication = new ExtHostAuthentication(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication); diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index f7e03c8d80f..b79db583bb0 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { mock, mockObject, MockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import * as editorRange from 'vs/editor/common/core/range'; -import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -603,7 +603,12 @@ suite('ExtHost Testing', () => { let req: TestRunRequest; let dto: TestRunDto; - const ext: IRelaxedExtensionDescription = {} as any; + + teardown(() => { + for (const { id } of c.trackers) { + c.disposeTestRun(id); + } + }); setup(async () => { proxy = mockObject()(); @@ -631,11 +636,11 @@ 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, configuration, cts.token)); assert.strictEqual(tracker.hasRunningTasks, false); - const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); - const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); + const task1 = c.createTestRun('ctrl', single, req, 'run1', true); + const task2 = c.createTestRun('ctrl', single, req, 'run2', true); assert.strictEqual(proxy.$startedExtensionTestRun.called, false); assert.strictEqual(tracker.hasRunningTasks, true); @@ -656,8 +661,8 @@ 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 task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); + const task = c.createTestRun('ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -681,8 +686,8 @@ suite('ExtHost Testing', () => { }); test('run cancel force ends on second cancellation request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, cts.token)); - const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); + const task = c.createTestRun('ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -700,7 +705,7 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from an extension request', () => { - const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); + const task1 = c.createTestRun('ctrl', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; assert.strictEqual(tracker.hasRunningTasks, true); @@ -716,8 +721,8 @@ suite('ExtHost Testing', () => { }] ]); - const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); - const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true); + const task2 = c.createTestRun('ctrl', single, req, 'run2', true); + const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true); task1.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); @@ -731,7 +736,7 @@ suite('ExtHost Testing', () => { }); test('adds tests to run smartly', () => { - const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); + const task1 = c.createTestRun('ctrlId', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; const expectedArgs: unknown[][] = []; assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); @@ -770,7 +775,7 @@ suite('ExtHost Testing', () => { const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt')); test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0)); single.root.children.replace([test1, test2]); - const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); + const task = c.createTestRun('ctrlId', single, req, 'hello world', false); const message1 = new TestMessage('some message'); message1.location = new Location(URI.file('/a.txt'), new Position(0, 0)); @@ -811,7 +816,7 @@ suite('ExtHost Testing', () => { }); test('guards calls after runs are ended', () => { - const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); + const task = c.createTestRun('ctrl', single, req, 'hello world', false); task.end(); task.failed(single.root, new TestMessage('some message')); @@ -823,7 +828,7 @@ suite('ExtHost Testing', () => { }); test('excludes tests outside tree or explicitly excluded', () => { - const task = c.createTestRun(ext, 'ctrlId', single, { + const task = c.createTestRun('ctrlId', single, { profile: configuration, include: [single.root.children.get('id-a')!], exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], @@ -852,7 +857,7 @@ suite('ExtHost Testing', () => { const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined); testB!.children.replace([childB]); - const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false); + const task1 = c.createTestRun('ctrl', single, new TestRunRequestImpl(), 'hello world', false); const tracker = Iterable.first(c.trackers)!; task1.passed(childA); diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index 727c4dea880..ab38b202b6e 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/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/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts b/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts index 2cc5a637a6a..5234d7abb34 100644 --- a/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts +++ b/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/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 1d903c40815..1502ef3f565 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/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/src/vs/workbench/browser/codeeditor.ts b/src/vs/workbench/browser/codeeditor.ts index 33358b10156..dcce3224ba9 100644 --- a/src/vs/workbench/browser/codeeditor.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -9,7 +9,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange } from 'vs/editor/common/core/range'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 59eba8e11ff..d56a31212ed 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -10,7 +10,7 @@ import { IComposite, ICompositeControl } from 'vs/workbench/common/composite'; import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { trackFocus, Dimension, IDomPosition, focusWindow } from 'vs/base/browser/dom'; +import { trackFocus, Dimension, IDomPosition } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertIsDefined } from 'vs/base/common/types'; @@ -36,7 +36,7 @@ export abstract class Composite extends Component implements IComposite { private readonly _onTitleAreaUpdate = this._register(new Emitter()); readonly onTitleAreaUpdate = this._onTitleAreaUpdate.event; - private _onDidFocus: Emitter | undefined; + protected _onDidFocus: Emitter | undefined; get onDidFocus(): Event { if (!this._onDidFocus) { this._onDidFocus = this.registerFocusTrackEvents().onDidFocus; @@ -45,10 +45,6 @@ export abstract class Composite extends Component implements IComposite { return this._onDidFocus.event; } - protected fireOnDidFocus(): void { - this._onDidFocus?.fire(); - } - private _onDidBlur: Emitter | undefined; get onDidBlur(): Event { if (!this._onDidBlur) { @@ -86,22 +82,16 @@ export abstract class Composite extends Component implements IComposite { protected actionRunner: IActionRunner | undefined; - private _telemetryService: ITelemetryService; - protected get telemetryService(): ITelemetryService { return this._telemetryService; } - - private visible: boolean; + private visible = false; private parent: HTMLElement | undefined; constructor( id: string, - telemetryService: ITelemetryService, + protected readonly telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService ) { super(id, themeService, storageService); - - this._telemetryService = telemetryService; - this.visible = false; } getTitle(): string | undefined { @@ -149,13 +139,7 @@ export abstract class Composite extends Component implements IComposite { * Called when this composite should receive keyboard focus. */ focus(): void { - const container = this.getContainer(); - if (container) { - // Make sure to focus the window of the container - // because it is possible that the composite is - // opened in a auxiliary window that is not focused. - focusWindow(container); - } + // Subclasses can implement } /** diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index c86801ca274..459d7de1200 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/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'; @@ -32,6 +32,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'; // --- Start Positron --- // eslint-disable-next-line no-duplicate-imports @@ -49,7 +50,7 @@ export class WorkbenchContextKeysHandler extends Disposable { private activeEditorAvailableEditorIds: IContextKey; private activeEditorIsReadonly: IContextKey; - private activeCompareEditorOriginalWritable: IContextKey; + private activeCompareEditorCanSwap: IContextKey; private activeEditorCanToggleReadonly: IContextKey; private activeEditorGroupEmpty: IContextKey; @@ -146,7 +147,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); @@ -344,13 +345,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/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 7d0b333bece..d5e1a7f3391 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -59,23 +59,23 @@ export class EditorPaneDescriptor implements IEditorPaneDescriptor { static readonly onWillInstantiateEditorPane = EditorPaneDescriptor._onWillInstantiateEditorPane.event; static create( - ctor: { new(...services: Services): EditorPane }, + ctor: { new(group: IEditorGroup, ...services: Services): EditorPane }, typeId: string, name: string ): EditorPaneDescriptor { - return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); + return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); } private constructor( - private readonly ctor: IConstructorSignature, + private readonly ctor: IConstructorSignature, readonly typeId: string, readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): EditorPane { + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): EditorPane { EditorPaneDescriptor._onWillInstantiateEditorPane.fire({ typeId: this.typeId }); - const pane = instantiationService.createInstance(this.ctor); + const pane = instantiationService.createInstance(this.ctor, group); EditorPaneDescriptor.instantiatedEditorPanes.add(this.typeId); return pane; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 0bb8699861a..75213bf3f02 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -574,7 +574,8 @@ class ResourceLabelWidget extends IconLabel { separator: this.options?.separator, domId: this.options?.domId, disabledCommand: this.options?.disabledCommand, - labelEscapeNewLines: this.options?.labelEscapeNewLines + labelEscapeNewLines: this.options?.labelEscapeNewLines, + descriptionTitle: this.options?.descriptionTitle, }; const resource = toResource(this.label); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 3cda0125ade..e4fc900871a 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; @@ -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'; // --- Start Positron --- @@ -201,6 +201,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; } @@ -226,11 +231,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; @@ -259,7 +259,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; @@ -422,9 +421,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); } @@ -465,12 +470,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))); })); } @@ -740,7 +748,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; @@ -1158,8 +1166,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 { @@ -1193,9 +1204,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) { // --- Start Positron --- @@ -1647,7 +1655,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi layout(): void { if (!this.disposed) { - this._mainContainerDimension = getClientArea(this.parent); + this._mainContainerDimension = getClientArea(this.state.runtime.mainWindowFullscreen ? + mainWindow.document.body : // in fullscreen mode, make sure to use element because + this.parent // in that case the workbench will span the entire site + ); this.logService.trace(`Layout#layout, height: ${this._mainContainerDimension.height}, width: ${this._mainContainerDimension.width}`); position(this.mainContainer, 0, 0, 0, 0, 'relative'); @@ -2886,7 +2897,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(SIDEBAR_PART_MINIMUM_WIDTH, Math.round(workbenchDimensions.width / 4)); // 170 mirrors minimumWidth in sidebarPart.ts. LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.round(workbenchDimensions.width * 0.45); // --- End Positron --- - 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; // --- Start Positron --- @@ -2984,7 +2995,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/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index 17182b6fa91..f17465a5e4a 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/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/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index db4862c7427..9299856d6ff 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/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/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 7ae271c65cc..086dcb51b2c 100644 --- a/src/vs/workbench/browser/part.ts +++ b/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/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 2e2047e8f1d..15e58a58179 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/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/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index a3da033a5f4..50bd194d4ce 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -14,21 +14,27 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ActiveAuxiliaryContext, AuxiliaryBarFocusContext } from 'vs/workbench/common/contextkeys'; -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 { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_TOP_ACTIVE_BORDER, ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_TOP_FOREGROUND, ACTIVITY_BAR_TOP_INACTIVE_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, @@ -139,21 +162,22 @@ 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), - activeBorderBottomColor: theme.getColor(PANEL_ACTIVE_TITLE_BORDER), - activeForegroundColor: theme.getColor(PANEL_ACTIVE_TITLE_FOREGROUND), - inactiveForegroundColor: theme.getColor(PANEL_INACTIVE_TITLE_FOREGROUND), + get activeBorderBottomColor() { return $this.getCompositeBarPosition() === CompositeBarPosition.TITLE ? theme.getColor(PANEL_ACTIVE_TITLE_BORDER) : theme.getColor(ACTIVITY_BAR_TOP_ACTIVE_BORDER); }, + get activeForegroundColor() { return $this.getCompositeBarPosition() === CompositeBarPosition.TITLE ? theme.getColor(PANEL_ACTIVE_TITLE_FOREGROUND) : theme.getColor(ACTIVITY_BAR_TOP_FOREGROUND); }, + get inactiveForegroundColor() { return $this.getCompositeBarPosition() === CompositeBarPosition.TITLE ? theme.getColor(PANEL_INACTIVE_TITLE_FOREGROUND) : theme.getColor(ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND); }, badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND), badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND), - dragAndDropBorder: theme.getColor(PANEL_DRAG_AND_DROP_BORDER) + get dragAndDropBorder() { return $this.getCompositeBarPosition() === CompositeBarPosition.TITLE ? theme.getColor(PANEL_DRAG_AND_DROP_BORDER) : theme.getColor(ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER); } }), compact: true }; @@ -166,15 +190,70 @@ 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; + } + + // TODO@benibenj chache this + 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/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index 32a2ef4370b..778f3700f0d 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -23,30 +23,76 @@ } /* End Positron */ +.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 > .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; } +.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 { + border-top-color: var(--vscode-activityBarTop-activeBorder) !important; +} + .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:hover .action-label, +.monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item:focus .action-label { + color: var(--vscode-activityBarTop-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/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 3725e594c97..dda7c882d5e 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -86,7 +86,7 @@ export class BannerPart extends Part implements IBannerService { })); // Track focus - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); BannerFocused.bindTo(scopedContextKeyService).set(true); return this.element; @@ -222,13 +222,12 @@ export class BannerPart extends Part implements IBannerService { } // Action - if (!item.disableCloseAction) { - const actionBarContainer = append(this.element, $('div.action-container')); - this.actionBar = this._register(new ActionBar(actionBarContainer)); - const closeAction = this._register(new Action('banner.close', 'Close Banner', ThemeIcon.asClassName(widgetClose), true, () => this.close(item))); - this.actionBar.push(closeAction, { icon: true, label: false }); - this.actionBar.setFocusable(false); - } + const actionBarContainer = append(this.element, $('div.action-container')); + this.actionBar = this._register(new ActionBar(actionBarContainer)); + const label = item.closeLabel ?? 'Close Banner'; + const closeAction = this._register(new Action('banner.close', label, ThemeIcon.asClassName(widgetClose), true, () => this.close(item))); + this.actionBar.push(closeAction, { icon: true, label: false }); + this.actionBar.setFocusable(false); this.setVisibility(true); this.item = item; diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 76d6c0c1ee4..a9fc39c84ed 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/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); } @@ -201,14 +202,14 @@ export class CompositeBar extends Widget implements ICompositeBar { create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild($('.composite-bar')); this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action instanceof CompositeOverflowActivityAction) { return this.compositeOverflowActionViewItem; } const item = this.model.findItem(action.id); return item && this.instantiationService.createInstance( CompositeActionViewItem, - { draggable: true, colors: this.options.colors, icon: this.options.icon, hoverOptions: this.options.activityHoverOptions, compact: this.options.compact }, + { ...options, draggable: true, colors: this.options.colors, icon: this.options.icon, hoverOptions: this.options.activityHoverOptions, compact: this.options.compact }, action as CompositeBarAction, item.pinnedAction, item.toggleBadgeAction, diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index fb06c28c67d..6ae11c51361 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/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 { @@ -706,12 +706,12 @@ export class CompositeActionViewItem extends CompositeBarActionViewItem { protected override updateChecked(): void { if (this.action.checked) { this.container.classList.add('checked'); - this.container.setAttribute('aria-label', this.container.title); + this.container.setAttribute('aria-label', this.getTooltip() ?? this.container.title); this.container.setAttribute('aria-expanded', 'true'); this.container.setAttribute('aria-selected', 'true'); } else { this.container.classList.remove('checked'); - this.container.setAttribute('aria-label', this.container.title); + this.container.setAttribute('aria-label', this.getTooltip() ?? this.container.title); this.container.setAttribute('aria-expanded', 'false'); this.container.setAttribute('aria-selected', 'false'); } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index b4d406a6364..d1617f55cb5 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -33,8 +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 { 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 { @@ -62,14 +63,14 @@ export abstract class CompositePart extends Part { protected toolBar: WorkbenchToolBar | undefined; protected titleLabelElement: HTMLElement | undefined; - protected readonly hoverDelegate: IHoverDelegate; + protected readonly toolbarHoverDelegate: IHoverDelegate; private readonly mapCompositeToCompositeContainer = new Map(); private readonly mapActionsBindingToComposite = new Map void>(); 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()); @@ -96,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.hoverDelegate = this._register(getDefaultHoverDelegate('element', true)); + this.toolbarHoverDelegate = this._register(createInstantHoverDelegate()); } protected openComposite(id: string, focus?: boolean): Composite | undefined { @@ -407,7 +408,7 @@ export abstract class CompositePart extends Part { anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment(), toggleMenuTitle: localize('viewsAndMoreActions', "Views and More Actions..."), telemetrySource: this.nameForTelemetry, - hoverDelegate: this.hoverDelegate + hoverDelegate: this.toolbarHoverDelegate })); this.collectCompositeActions()(); @@ -419,6 +420,7 @@ export abstract class CompositePart extends Part { const titleContainer = append(parent, $('.title-label')); const titleLabel = append(titleContainer, $('h2')); this.titleLabelElement = titleLabel; + const hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), titleLabel, '')); const $this = this; return { @@ -426,7 +428,7 @@ export abstract class CompositePart extends Part { // The title label is shared for all composites in the base CompositePart if (!this.activeComposite || this.activeComposite.getId() === id) { titleLabel.innerText = title; - titleLabel.title = keybinding ? localize('titleTooltip', "{0} ({1})", title, keybinding) : title; + hover.update(keybinding ? localize('titleTooltip', "{0} ({1})", title, keybinding) : title); } }, @@ -436,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/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index 7e4dd2aeddf..f6f50db5041 100644 --- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -171,6 +171,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) { + // 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. + const canMoveVeto = editor.canMove(group.id, this.editorPartsView.mainPart.activeGroup.id); + if (typeof canMoveVeto === 'string') { + group.openEditor(editor); + event.veto(canMoveVeto); + break; + } + } + } + })); // Layout: specifically `onWillLayout` to have a chance // to build the aux editor part before other components diff --git a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index bcd00491191..f4314ae0dbb 100644 --- a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -13,7 +13,7 @@ import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/bina import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; /** @@ -24,6 +24,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { static override readonly ID = BINARY_DIFF_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -33,7 +34,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); + super(group, telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); } getMetadata(): string | undefined { diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index cba829c7be5..52a01908818 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -13,6 +13,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ByteSize } from 'vs/platform/files/common/files'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise; @@ -33,12 +34,13 @@ export abstract class BaseBinaryResourceEditor extends EditorPlaceholder { constructor( id: string, + group: IEditorGroup, private readonly callbacks: IOpenCallbacks, telemetryService: ITelemetryService, themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } override getTitle(): string { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 2a9d829b3ff..e2fefb174e6 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/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/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 26f77202d53..13c2df31119 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/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/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts new file mode 100644 index 00000000000..7a782b9ee3e --- /dev/null +++ b/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/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index ca21ad13864..1611076ea51 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/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/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index ddcce4134c0..66940e29b77 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -80,7 +80,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution if (workingCopyResult?.condition === condition) { if ( workingCopyResult.workingCopy.isDirty() && - this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource, workingCopyResult.reason).mode !== AutoSaveMode.OFF ) { this.discardAutoSave(workingCopyResult.workingCopy); @@ -96,7 +96,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution editorResult?.condition === condition && !editorResult.editor.editor.isDisposed() && editorResult.editor.editor.isDirty() && - this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor, editorResult.reason).mode !== AutoSaveMode.OFF ) { this.waitingOnConditionAutoSaveEditors.delete(resource); @@ -151,7 +151,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution return; // no auto save for non-dirty, readonly or untitled editors } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { // Determine if we need to save all. In case of a window focus change we also save if // auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change) @@ -198,7 +198,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution continue; // we never auto save untitled working copies } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { @@ -257,12 +257,13 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Save if dirty and unless prevented by other conditions such as error markers if (workingCopy.isDirty()) { - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const reason = SaveReason.AUTO; + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(), workingCopy.typeId); - workingCopy.save({ reason: SaveReason.AUTO }); + workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { - this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason: SaveReason.AUTO, condition: autoSaveMode.reason }); + this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason, condition: autoSaveMode.reason }); } } }, autoSaveAfterDelay); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 68cd3a6b328..89795c71057 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/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/src/vs/workbench/browser/parts/editor/editorConfiguration.ts b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts index 9f3008f7385..0df198dd6de 100644 --- a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts +++ b/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/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 685f4d762f7..ccff161149f 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, focusWindow, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, getWindow, getActiveElement } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -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; @@ -653,17 +660,27 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return true; } + private toResourceTelemetryDescriptor(resource: URI): object | undefined { + if (!resource) { + return undefined; + } + const path = resource ? resource.scheme === Schemas.file ? resource.fsPath : resource.path : undefined; + if (!path) { + return undefined; + } + let resourceExt = extname(resource); + // Remove query parameters from the resource extension + const queryStringLocation = resourceExt.indexOf('?'); + resourceExt = queryStringLocation !== -1 ? resourceExt.substr(0, queryStringLocation) : resourceExt; + return { mimeType: new TelemetryTrustedValue(getMimeTypes(resource).join(', ')), scheme: resource.scheme, ext: resourceExt, path: hash(path) }; + } + private toEditorTelemetryDescriptor(editor: EditorInput): object { const descriptor = editor.getTelemetryDescriptor(); - const resource = EditorResourceAccessor.getOriginalUri(editor); - const path = resource ? resource.scheme === Schemas.file ? resource.fsPath : resource.path : undefined; - if (resource && path) { - let resourceExt = extname(resource); - // Remove query parameters from the resource extension - const queryStringLocation = resourceExt.indexOf('?'); - resourceExt = queryStringLocation !== -1 ? resourceExt.substr(0, queryStringLocation) : resourceExt; - descriptor['resource'] = { mimeType: new TelemetryTrustedValue(getMimeTypes(resource).join(', ')), scheme: resource.scheme, ext: resourceExt, path: hash(path) }; + const resource = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.BOTH }); + if (URI.isUri(resource)) { + descriptor['resource'] = this.toResourceTelemetryDescriptor(resource); /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { @@ -671,6 +688,20 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } */ return descriptor; + } else if (resource) { + if (resource.primary) { + descriptor['resource'] = this.toResourceTelemetryDescriptor(resource.primary); + } + if (resource.secondary) { + descriptor['resourceSecondary'] = this.toResourceTelemetryDescriptor(resource.secondary); + } + /* __GDPR__FRAGMENT__ + "EditorTelemetryDescriptor" : { + "resource": { "${inline}": [ "${URIDescriptor}" ] }, + "resourceSecondary": { "${inline}": [ "${URIDescriptor}" ] } + } + */ + return descriptor; } return descriptor; @@ -714,7 +745,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close active one last if (activeEditor) { - this.doCloseEditor(activeEditor); + this.doCloseEditor(activeEditor, true); } } @@ -762,6 +793,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 +816,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 +940,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 +1001,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 +1088,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 +1097,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 }; @@ -1215,7 +1271,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region moveEditor() - moveEditors(editors: { editor: EditorInput; options?: IEditorOptions }[], target: EditorGroupView): void { + moveEditors(editors: { editor: EditorInput; options?: IEditorOptions }[], target: EditorGroupView): boolean { // Optimization: knowing that we move many editors, we // delay the title update to a later point for this group @@ -1226,29 +1282,38 @@ export class EditorGroupView extends Themable implements IEditorGroupView { skipTitleUpdate: this !== target }; + let moveFailed = false; + + const movedEditors = new Set(); for (const { editor, options } of editors) { - this.moveEditor(editor, target, options, internalOptions); + if (this.moveEditor(editor, target, options, internalOptions)) { + movedEditors.add(editor); + } else { + moveFailed = true; + } } // Update the title control all at once with all editors // in source and target if the title update was skipped if (internalOptions.skipTitleUpdate) { - const movedEditors = editors.map(({ editor }) => editor); - target.titleControl.openEditors(movedEditors); - this.titleControl.closeEditors(movedEditors); + target.titleControl.openEditors(Array.from(movedEditors)); + this.titleControl.closeEditors(Array.from(movedEditors)); } + + return !moveFailed; } - moveEditor(editor: EditorInput, target: EditorGroupView, options?: IEditorOptions, internalOptions?: IInternalMoveCopyOptions): void { + moveEditor(editor: EditorInput, target: EditorGroupView, options?: IEditorOptions, internalOptions?: IInternalMoveCopyOptions): boolean { // Move within same group if (this === target) { this.doMoveEditorInsideGroup(editor, options); + return true; } // Move across groups else { - this.doMoveOrCopyEditorAcrossGroups(editor, target, options, { ...internalOptions, keepCopy: false }); + return this.doMoveOrCopyEditorAcrossGroups(editor, target, options, { ...internalOptions, keepCopy: false }); } } @@ -1289,9 +1354,19 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - private doMoveOrCopyEditorAcrossGroups(editor: EditorInput, target: EditorGroupView, openOptions?: IEditorOpenOptions, internalOptions?: IInternalMoveCopyOptions): void { + private doMoveOrCopyEditorAcrossGroups(editor: EditorInput, target: EditorGroupView, openOptions?: IEditorOpenOptions, internalOptions?: IInternalMoveCopyOptions): boolean { const keepCopy = internalOptions?.keepCopy; + // Validate that we can move + if (!keepCopy || editor.hasCapability(EditorInputCapabilities.Singleton) /* singleton editors will always move */) { + const canMoveVeto = editor.canMove(this.id, target.id); + if (typeof canMoveVeto === 'string') { + this.dialogService.error(canMoveVeto, localize('moveErrorDetails', "Try saving or reverting the editor first and then try again.")); + + return false; + } + } + // 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 @@ -1317,6 +1392,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (!keepCopy) { this.doCloseEditor(editor, true /* do not focus next one behind if any */, { ...internalOptions, context: EditorCloseContext.MOVE }); } + + return true; } //#endregion diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 3daa20701a7..77d8a445857 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/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/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index 0f26fa1aa20..7a73fe46fc5 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -24,6 +24,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; +import { getWindowById } from 'vs/base/browser/dom'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -70,8 +71,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { protected _options: IEditorOptions | undefined; get options(): IEditorOptions | undefined { return this._options; } - private _group: IEditorGroup | undefined; - get group(): IEditorGroup | undefined { return this._group; } + get window() { return getWindowById(this.group.windowId, true).window; } /** * Should be overridden by editors that have their own ScopedContextKeyService @@ -80,6 +80,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { constructor( id: string, + readonly group: IEditorGroup, telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService @@ -145,22 +146,20 @@ export abstract class EditorPane extends Composite implements IEditorPane { this._options = options; } - override setVisible(visible: boolean, group?: IEditorGroup): void { + override setVisible(visible: boolean): void { super.setVisible(visible); // Propagate to Editor - this.setEditorVisible(visible, group); + this.setEditorVisible(visible); } /** - * Indicates that the editor control got visible or hidden in a specific group. A - * editor instance will only ever be visible in one editor group. + * Indicates that the editor control got visible or hidden. * * @param visible the state of visibility of this editor - * @param group the editor group this editor is in. */ - protected setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - this._group = group; + protected setEditorVisible(visible: boolean): void { + // Subclasses can implement } setBoundarySashes(_sashes: IBoundarySashes) { diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 1094f315842..5d638871273 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/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/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index 417314bc1f7..7a7691b3850 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -29,6 +29,7 @@ import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IEditorPlaceholderContents { icon: string; @@ -55,11 +56,12 @@ export abstract class EditorPlaceholder extends EditorPane { constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { @@ -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/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index e1ee2bc0d94..dc58c035fed 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/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/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 218c8808205..d7a82ed5c16 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/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/src/vs/workbench/browser/parts/editor/editorWithViewState.ts b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts index f01bedd8f59..e31756007e9 100644 --- a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts +++ b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts @@ -31,6 +31,7 @@ export abstract class AbstractEditorWithViewState extends Edit constructor( id: string, + group: IEditorGroup, viewStateStorageKey: string, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @@ -40,17 +41,17 @@ export abstract class AbstractEditorWithViewState extends Edit @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); this.viewState = this.getEditorMemento(editorGroupService, textResourceConfigurationService, viewStateStorageKey, 100); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { // Listen to close events to trigger `onWillCloseEditorInGroup` - this.groupListener.value = group?.onWillCloseEditor(e => this.onWillCloseEditor(e)); + this.groupListener.value = this.group.onWillCloseEditor(e => this.onWillCloseEditor(e)); - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } private onWillCloseEditor(e: IEditorCloseEvent): void { @@ -110,7 +111,7 @@ export abstract class AbstractEditorWithViewState extends Edit // - the user configured to not restore view state unless the editor is still opened in the group if ( (input.isDisposed() && !this.tracksDisposedEditorViewState()) || - (!this.shouldRestoreEditorViewState(input) && (!this.group || !this.group.contains(input))) + (!this.shouldRestoreEditorViewState(input) && !this.group.contains(input)) ) { this.clearEditorViewState(resource, this.group); } @@ -147,10 +148,6 @@ export abstract class AbstractEditorWithViewState extends Edit } private saveEditorViewState(resource: URI): void { - if (!this.group) { - return; - } - const editorViewState = this.computeEditorViewState(resource); if (!editorViewState) { return; @@ -160,7 +157,7 @@ export abstract class AbstractEditorWithViewState extends Edit } protected loadEditorViewState(input: EditorInput | undefined, context?: IEditorOpenContext): T | undefined { - if (!input || !this.group) { + if (!input) { return undefined; // we need valid input } diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 1a6b5ca985e..24d85415eb8 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -36,19 +36,23 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, stickyModel)); this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, unstickyModel)); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } - private handlePinnedTabsSeparateRowToolbars(): void { + private handlePinnedTabsLayoutChange(): void { if (this.groupView.count === 0) { // Do nothing as no tab bar is visible return; } + + const hadTwoTabBars = this.parent.classList.contains('two-tab-bars'); + const hasTwoTabBars = this.groupView.count !== this.groupView.stickyCount && this.groupView.stickyCount > 0; + // Ensure action toolbar is only visible once - if (this.groupView.count === this.groupView.stickyCount) { - this.parent.classList.toggle('two-tab-bars', false); - } else { - this.parent.classList.toggle('two-tab-bars', true); + this.parent.classList.toggle('two-tab-bars', hasTwoTabBars); + + if (hadTwoTabBars !== hasTwoTabBars) { + this.groupView.relayout(); } } @@ -85,7 +89,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleOpenedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } beforeCloseEditor(editor: EditorInput): void { @@ -111,7 +115,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleClosedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void { @@ -125,7 +129,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.openEditor(editor); } - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } else { if (this.model.isSticky(editor)) { @@ -144,14 +148,14 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.closeEditor(editor); this.stickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } unstickEditor(editor: EditorInput): void { this.stickyEditorTabsControl.closeEditor(editor); this.unstickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } setActive(isActive: boolean): void { diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index becfd13cac2..c3d7a0cce9d 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -122,6 +122,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState extends return this.editorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.editorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 21429e37334..3c298c26b1e 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -58,6 +58,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -68,7 +69,7 @@ export class TextDiffEditor extends AbstractTextEditor imp @IFileService fileService: IFileService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); + super(TextDiffEditor.ID, group, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); } override getTitle(): string { @@ -171,7 +172,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "At least one file is not displayed in the text compare editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -222,7 +223,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Replace this editor with the binary one - (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + this.group.replaceEditors([{ editor: input, replacement: binaryDiffInput, options: { @@ -232,8 +233,8 @@ export class TextDiffEditor extends AbstractTextEditor imp // and do not control the initial intent that resulted // in us now opening as binary. activation: EditorActivation.PRESERVE, - pinned: this.group?.isPinned(input), - sticky: this.group?.isSticky(input) + pinned: this.group.isPinned(input), + sticky: this.group.isSticky(input) } }]); } @@ -365,8 +366,8 @@ export class TextDiffEditor extends AbstractTextEditor imp return this.diffEditorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.diffEditorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 563f12a1be7..629d430bbec 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/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,37 @@ 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 { + // The top position can vary depending on the view zones (find widget for example) + scrollTop: editor.getScrollTop() - editor.getTopForLineNumber(1), + 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/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 7a16fa667b8..0a3b885e01d 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -18,7 +18,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ScrollType, ICodeEditorViewState } from 'vs/editor/common/editorCommon'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/model'; @@ -37,6 +37,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -46,7 +47,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< @IEditorService editorService: IEditorService, @IFileService fileService: IFileService ) { - super(id, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(id, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -130,6 +131,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { static readonly ID = 'workbench.editors.textResourceEditor'; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -141,7 +143,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { @ILanguageService private readonly languageService: ILanguageService, @IFileService fileService: IFileService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(TextResourceEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void { diff --git a/src/vs/workbench/browser/parts/globalCompositeBar.ts b/src/vs/workbench/browser/parts/globalCompositeBar.ts index db3471fcadc..8301e27c643 100644 --- a/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -43,6 +43,7 @@ import { isString } from 'vs/base/common/types'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND } from 'vs/workbench/common/theme'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export class GlobalCompositeBar extends Disposable { @@ -72,15 +73,16 @@ export class GlobalCompositeBar extends Disposable { anchorAxisAlignment: AnchorAxisAlignment.HORIZONTAL }); this.globalActivityActionBar = this._register(new ActionBar(this.element, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action.id === GLOBAL_ACTIVITY_ID) { - return this.instantiationService.createInstance(GlobalActivityActionViewItem, this.contextMenuActionsProvider, { colors: this.colors, hoverOptions: this.activityHoverOptions }, contextMenuAlignmentOptions); + return this.instantiationService.createInstance(GlobalActivityActionViewItem, this.contextMenuActionsProvider, { ...options, colors: this.colors, hoverOptions: this.activityHoverOptions }, contextMenuAlignmentOptions); } if (action.id === ACCOUNTS_ACTIVITY_ID) { return this.instantiationService.createInstance(AccountsActivityActionViewItem, this.contextMenuActionsProvider, { + ...options, colors: this.colors, hoverOptions: this.activityHoverOptions }, @@ -308,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, @@ -390,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)) { @@ -407,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); @@ -627,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, @@ -637,7 +644,7 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV }), hoverOptions, compact: true, - }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService); + }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService); } } diff --git a/src/vs/workbench/browser/parts/media/compositepart.css b/src/vs/workbench/browser/parts/media/compositepart.css index fde7cdb706c..98f07d9cf18 100644 --- a/src/vs/workbench/browser/parts/media/compositepart.css +++ b/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/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 9250cd44de1..52baa5324f7 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/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/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index e4cbaff2272..40bcde5fdb4 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -173,7 +173,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente const notificationsToolBar = this._register(new ActionBar(toolbarContainer, { ariaLabel: localize('notificationsToolbar', "Notification Center Actions"), actionRunner, - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action.id === ConfigureDoNotDisturbAction.ID) { return this._register(this.instantiationService.createInstance(DropdownMenuActionViewItem, action, { getActions() { @@ -208,6 +208,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente return actions; }, }, this.contextMenuService, { + ...options, actionRunner, classNames: action.class, keybindingProvider: action => this.keybindingService.lookupKeybinding(action.id) diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 6f205b9d81f..84507d095dc 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/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/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts index 97d1d6a18c8..1a149ef7173 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ b/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/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 484ee51adb4..246532ddd02 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -29,6 +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/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -235,7 +237,7 @@ export class NotificationRenderer implements IListRenderer { + actionViewItemProvider: (action, options) => { if (action instanceof ConfigureNotificationAction) { return data.toDispose.add(new DropdownMenuActionViewItem(action, { getActions() { @@ -262,6 +264,7 @@ export class NotificationRenderer implements IListRenderer this.openerService.open(URI.parse(link), { allowCommands: true }), @@ -425,11 +430,8 @@ export class NotificationTemplateRenderer extends Disposable { })); const messageOverflows = notification.canCollapse && !notification.expanded && this.template.message.scrollWidth > this.template.message.clientWidth; - if (messageOverflows) { - this.template.message.title = this.template.message.textContent + ''; - } else { - this.template.message.removeAttribute('title'); - } + + customHover.update(messageOverflows ? this.template.message.textContent + '' : ''); return messageOverflows; } @@ -470,13 +472,13 @@ export class NotificationTemplateRenderer extends Disposable { actions.forEach(action => this.template.toolbar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) })); } - private renderSource(notification: INotificationViewItem): void { + private renderSource(notification: INotificationViewItem, sourceCustomHover: ICustomHover): void { if (notification.expanded && notification.source) { this.template.source.textContent = localize('notificationSource', "Source: {0}", notification.source); - this.template.source.title = notification.source; + sourceCustomHover.update(notification.source); } else { this.template.source.textContent = ''; - this.template.source.removeAttribute('title'); + sourceCustomHover.update(''); } } diff --git a/src/vs/workbench/browser/parts/paneCompositeBar.ts b/src/vs/workbench/browser/parts/paneCompositeBar.ts index 8fe901376cc..94dce01b958 100644 --- a/src/vs/workbench/browser/parts/paneCompositeBar.ts +++ b/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/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 4b4208ae826..9347fa945ec 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/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))); @@ -307,7 +318,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart this.keybindingService.lookupKeybinding(action.id), anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment(), toggleMenuTitle: localize('moreActions', "More Actions..."), - hoverDelegate: this.hoverDelegate + hoverDelegate: this.toolbarHoverDelegate })); this.updateGlobalToolbarActions(); @@ -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); } @@ -464,16 +552,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() : []; @@ -519,6 +608,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) { @@ -536,5 +642,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/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 1efc42d9040..0de72c304b3 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/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'; @@ -499,7 +499,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/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 6b19e414ae4..cc47014c900 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/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'; @@ -195,10 +195,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/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 65f962e8241..0f2312e1f19 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/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/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 3217d58743d..368f75e3ed2 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/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'; @@ -121,12 +121,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(); } @@ -162,7 +169,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); } @@ -176,7 +183,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(); @@ -187,7 +194,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), @@ -203,7 +210,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 { @@ -213,6 +221,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) { @@ -223,16 +242,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/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index e18622523c5..76af9a42b7f 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/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/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 664a333bb16..f938ea7b353 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -338,7 +338,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.element = parent; // Track focus within container - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); StatusBarFocused.bindTo(scopedContextKeyService).set(true); // Left items container @@ -675,6 +675,9 @@ export class StatusbarService extends MultiWindowParts implements readonly mainPart = this._register(this.instantiationService.createInstance(MainStatusbarPart)); + private readonly _onDidCreateAuxiliaryStatusbarPart = this._register(new Emitter()); + private readonly onDidCreateAuxiliaryStatusbarPart = this._onDidCreateAuxiliaryStatusbarPart.event; + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -688,6 +691,8 @@ export class StatusbarService extends MultiWindowParts implements //#region Auxiliary Statusbar Parts createAuxiliaryStatusbarPart(container: HTMLElement): IAuxiliaryStatusbarPart { + + // Container const statusbarPartContainer = document.createElement('footer'); statusbarPartContainer.classList.add('part', 'statusbar'); statusbarPartContainer.setAttribute('role', 'status'); @@ -696,6 +701,7 @@ export class StatusbarService extends MultiWindowParts implements statusbarPartContainer.setAttribute('tabindex', '0'); container.appendChild(statusbarPartContainer); + // Statusbar Part const statusbarPart = this.instantiationService.createInstance(AuxiliaryStatusbarPart, statusbarPartContainer); const disposable = this.registerPart(statusbarPart); @@ -703,6 +709,9 @@ export class StatusbarService extends MultiWindowParts implements Event.once(statusbarPart.onWillDispose)(() => disposable.dispose()); + // Emit internal event + this._onDidCreateAuxiliaryStatusbarPart.fire(statusbarPart); + return statusbarPart; } @@ -717,9 +726,46 @@ export class StatusbarService extends MultiWindowParts implements readonly onDidChangeEntryVisibility = this.mainPart.onDidChangeEntryVisibility; addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor { + if (entry.showInAllWindows) { + return this.doAddEntryToAllWindows(entry, id, alignment, priorityOrLocation); + } + return this.mainPart.addEntry(entry, id, alignment, priorityOrLocation); } + private doAddEntryToAllWindows(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor { + const entryDisposables = new DisposableStore(); + + const accessors = new Set(); + + function addEntry(part: StatusbarPart | AuxiliaryStatusbarPart): void { + const partDisposables = new DisposableStore(); + partDisposables.add(part.onWillDispose(() => partDisposables.dispose())); + + const accessor = partDisposables.add(part.addEntry(entry, id, alignment, priorityOrLocation)); + accessors.add(accessor); + partDisposables.add(toDisposable(() => accessors.delete(accessor))); + + entryDisposables.add(partDisposables); + partDisposables.add(toDisposable(() => entryDisposables.delete(partDisposables))); + } + + for (const part of this.parts) { + addEntry(part); + } + + entryDisposables.add(this.onDidCreateAuxiliaryStatusbarPart(part => addEntry(part))); + + return { + update: (entry: IStatusbarEntry) => { + for (const update of accessors) { + update.update(entry); + } + }, + dispose: () => entryDisposables.dispose() + }; + } + isEntryVisible(id: string): boolean { return this.mainPart.isEntryVisible(id); } diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 3843068e941..54eded7aab3 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -5,8 +5,9 @@ import { isActiveDocument, reset } from 'vs/base/browser/dom'; import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { 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'; @@ -46,11 +47,11 @@ export class CommandCenterControl { primaryGroup: () => true, }, telemetrySource: 'commandCenter', - actionViewItemProvider: (action) => { + actionViewItemProvider: (action, options) => { if (action instanceof SubmenuItemAction && action.item.submenu === MenuId.CommandCenterCenter) { - return instantiationService.createInstance(CommandCenterCenterViewItem, action, windowTitle, hoverDelegate, {}); + return instantiationService.createInstance(CommandCenterCenterViewItem, action, windowTitle, { ...options, hoverDelegate }); } else { - return createActionViewItem(instantiationService, action, { hoverDelegate }); + return createActionViewItem(instantiationService, action, { ...options, hoverDelegate }); } } }); @@ -75,16 +76,18 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { private static readonly _quickOpenCommandId = 'workbench.action.quickOpenWithModes'; + private readonly _hoverDelegate: IHoverDelegate; + constructor( private readonly _submenu: SubmenuItemAction, private readonly _windowTitle: WindowTitle, - private readonly _hoverDelegate: IHoverDelegate, options: IBaseActionViewItemOptions, @IKeybindingService private _keybindingService: IKeybindingService, @IInstantiationService private _instaService: IInstantiationService, @IEditorGroupsService private _editorGroupService: IEditorGroupsService, ) { super(undefined, _submenu.actions.find(action => action.id === 'workbench.action.quickOpenWithModes') ?? _submenu.actions[0], options); + this._hoverDelegate = options.hoverDelegate ?? getDefaultHoverDelegate('mouse'); } override render(container: HTMLElement): void { diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index a3d09742994..5b8cf1a3a7e 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -25,7 +25,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { MenuBar, IMenuBarOptions } from 'vs/base/browser/ui/menu/menubar'; -import { Direction } from 'vs/base/browser/ui/menu/menu'; +import { HorizontalDirection, IMenuDirection, VerticalDirection } from 'vs/base/browser/ui/menu/menu'; import { mnemonicMenuLabel, unmnemonicLabel } from 'vs/base/common/labels'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { isFullscreen, onDidChangeFullscreen } from 'vs/base/browser/browser'; @@ -41,6 +41,7 @@ import { isICommandActionToggleInfo } from 'vs/platform/action/common/action'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { defaultMenuStyles } from 'vs/platform/theme/browser/defaultStyles'; import { mainWindow } from 'vs/base/browser/window'; +import { ActivityBarPosition } from 'vs/workbench/services/layout/browser/layoutService'; export type IOpenRecentAction = IAction & { uri: URI; remoteAuthority?: string }; @@ -189,7 +190,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 +475,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: @@ -536,14 +537,19 @@ export class CustomMenubarControl extends MenubarControl { return enableMenuBarMnemonics && (!isWeb || isFullscreen(mainWindow)); } - private get currentCompactMenuMode(): Direction | undefined { + private get currentCompactMenuMode(): IMenuDirection | undefined { if (this.currentMenubarVisibility !== 'compact') { return undefined; } // Menu bar lives in activity bar and should flow based on its location const currentSidebarLocation = this.configurationService.getValue('workbench.sideBar.location'); - return currentSidebarLocation === 'right' ? Direction.Left : Direction.Right; + const horizontalDirection = currentSidebarLocation === 'right' ? HorizontalDirection.Left : HorizontalDirection.Right; + + const activityBarLocation = this.configurationService.getValue('workbench.activityBar.location'); + const verticalDirection = activityBarLocation === ActivityBarPosition.BOTTOM ? VerticalDirection.Above : VerticalDirection.Below; + + return { horizontal: horizontalDirection, vertical: verticalDirection }; } private onDidVisibilityChange(visible: boolean): void { diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index e68c59f30df..59847c913e7 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -120,7 +120,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/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 8419ea065b1..2dde38122c6 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/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)); } @@ -532,7 +532,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { // --- Editor Actions const activeEditorPane = this.editorGroupsContainer.activeGroup?.activeEditorPane; if (activeEditorPane && activeEditorPane instanceof EditorPane) { - const result = activeEditorPane.getActionViewItem(action, { hoverDelegate: this.hoverDelegate }); + const result = activeEditorPane.getActionViewItem(action, options); if (result) { return result; @@ -540,7 +540,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } // Check extensions - return createActionViewItem(this.instantiationService, action, { ...options, hoverDelegate: this.hoverDelegate, menuAsChild: false }); + return createActionViewItem(this.instantiationService, action, { ...options, menuAsChild: false }); } private getKeybinding(action: IAction): ResolvedKeybinding | undefined { @@ -565,7 +565,8 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { anchorAlignmentProvider: () => AnchorAlignment.RIGHT, telemetrySource: 'titlePart', highlightToggledItems: this.editorActionsEnabled, // Only show toggled state for editor actions (Layout actions are not shown as toggled) - actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options) + actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options), + hoverDelegate: this.hoverDelegate })); if (this.editorActionsEnabled) { @@ -730,7 +731,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/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 3ec39eeafc2..e025f5c4d6c 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -27,6 +27,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getWindowById } from 'vs/base/browser/dom'; +import { CodeWindow } from 'vs/base/browser/window'; const enum WindowSettingNames { titleSeparator = 'window.titleSeparator', @@ -79,8 +81,10 @@ export class WindowTitle extends Disposable { private readonly editorService: IEditorService; + private readonly windowId: number; + constructor( - private readonly targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IConfigurationService protected readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -95,6 +99,7 @@ export class WindowTitle extends Disposable { super(); this.editorService = editorService.createScoped(editorGroupsContainer, this._store); + this.windowId = targetWindow.vscodeWindowId; this.updateTitleIncludesFocusedView(); this.registerListeners(); @@ -177,7 +182,8 @@ export class WindowTitle extends Disposable { nativeTitle = this.productService.nameLong; } - if (!this.targetWindow.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { + const window = getWindowById(this.windowId, true).window; + if (!window.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { // TODO@electron macOS: if we set a window title for // the first time and it matches the one we set in // `windowImpl.ts` somehow the window does not appear @@ -185,10 +191,10 @@ export class WindowTitle extends Disposable { // briefly to something different to ensure macOS // recognizes we have a window. // See: https://github.com/microsoft/vscode/issues/191288 - this.targetWindow.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; + window.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; } - this.targetWindow.document.title = nativeTitle; + window.document.title = nativeTitle; this.title = title; this.onDidChangeEmitter.fire(); diff --git a/src/vs/workbench/browser/parts/views/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts index 849966bbe33..62058fd74fc 100644 --- a/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/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/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 15c4a1aae00..9e343669291 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -18,7 +18,7 @@ /* Misc */ .file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight, -.monaco-tl-contents .monaco-highlighted-label .highlight { +.pane-body .monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: var(--vscode-list-filterMatchBackground); outline: 1px dotted var(--vscode-list-filterMatchBorder); @@ -268,7 +268,7 @@ } .viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls > .viewpane-filter-badge { - margin: 4px 0px; + margin: 4px 2px 4px 0px; padding: 0px 8px; border-radius: 2px; } @@ -278,10 +278,6 @@ display: none; } -.viewpane-filter > .viewpane-filter-controls > .monaco-action-bar .action-item .action-label.codicon.filter { - padding: 2px; -} - .panel > .title .monaco-action-bar .action-item.viewpane-filter-container { max-width: 400px; min-width: 150px; diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 0eb478a3544..661638fa561 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/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/src/vs/workbench/browser/parts/views/viewFilter.ts b/src/vs/workbench/browser/parts/views/viewFilter.ts index 3e5763a2203..b6285e45c71 100644 --- a/src/vs/workbench/browser/parts/views/viewFilter.ts +++ b/src/vs/workbench/browser/parts/views/viewFilter.ts @@ -25,6 +25,7 @@ import { SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntr import { Widget } from 'vs/base/browser/ui/widget'; import { Emitter } from 'vs/base/common/event'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; const viewFilterMenu = new MenuId('menu.view.filter'); export const viewFilterSubmenu = new MenuId('submenu.view.filter'); @@ -196,9 +197,9 @@ export class FilterWidget extends Widget { return this.instantiationService.createInstance(MenuWorkbenchToolBar, container, viewFilterMenu, { hiddenItemStrategy: HiddenItemStrategy.NoHide, - actionViewItemProvider: (action: IAction) => { + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof SubmenuItemAction && action.item.submenu.id === viewFilterSubmenu.id) { - this.moreFiltersActionViewItem = this.instantiationService.createInstance(MoreFiltersActionViewItem, action, undefined); + this.moreFiltersActionViewItem = this.instantiationService.createInstance(MoreFiltersActionViewItem, action, options); this.moreFiltersActionViewItem.checked = this.isMoreFiltersChecked; return this.moreFiltersActionViewItem; } diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 978baf437cd..294c69b1448 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { asCssVariable, foreground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault, focusWindow } from 'vs/base/browser/dom'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault } from 'vs/base/browser/dom'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -47,6 +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/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. */ @@ -64,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 { @@ -331,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; @@ -340,8 +349,11 @@ 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; private viewWelcomeController!: ViewWelcomeController; @@ -364,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)); @@ -519,13 +532,14 @@ export abstract class ViewPane extends Pane implements IView { } const calculatedTitle = this.calculateTitle(title); - this.titleContainer = append(container, $('h3.title', { title: calculatedTitle }, calculatedTitle)); + this.titleContainer = append(container, $('h3.title', {}, calculatedTitle)); + this.titleContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle)); if (this._titleDescription) { this.setTitleDescription(this._titleDescription); } - this.iconContainer.title = calculatedTitle; + this.iconContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.iconContainer, calculatedTitle)); this.iconContainer.setAttribute('aria-label', calculatedTitle); } @@ -533,11 +547,11 @@ 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) { - this.iconContainer.title = calculatedTitle; + this.iconContainerHover?.update(calculatedTitle); this.iconContainer.setAttribute('aria-label', calculatedTitle); } @@ -548,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)); } } @@ -596,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; } @@ -623,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/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index f575fa95242..67b7f28268f 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/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/src/vs/workbench/browser/quickaccess.ts b/src/vs/workbench/browser/quickaccess.ts index 6b9e07abe7a..aec2065963d 100644 --- a/src/vs/workbench/browser/quickaccess.ts +++ b/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/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 23f2e2bac99..8fab9bc5b71 100644 --- a/src/vs/workbench/browser/style.ts +++ b/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/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 50d76a2213c..196edf88796 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -142,6 +142,13 @@ export interface IWorkbenchConstructionOptions { */ readonly remoteAuthority?: string; + /** + * The server base path is the path where the workbench is served from. + * The path must be absolute (start with a slash). + * Corresponds to option `server-base-path` on the server side. + */ + readonly serverBasePath?: string; + /** * The connection token to send to the server. */ diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 9061af27859..6250a90d6c1 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -284,11 +284,11 @@ export class BrowserMain extends Disposable { // Register them early because they are needed for the profiles initialization await this.registerIndexedDBFileSystemProviders(environmentService, fileService, logService, loggerService, logsPath); - // Remote + const connectionToken = environmentService.options.connectionToken || getCookieValue(connectionTokenCookieName); const remoteResourceLoader = this.configuration.remoteResourceProvider ? new BrowserRemoteResourceLoader(fileService, this.configuration.remoteResourceProvider) : undefined; const resourceUriProvider = this.configuration.resourceUriProvider ?? remoteResourceLoader?.getResourceUriProvider(); - const remoteAuthorityResolverService = new RemoteAuthorityResolverService(!environmentService.expectsResolverExtension, connectionToken, resourceUriProvider, productService, logService); + const remoteAuthorityResolverService = new RemoteAuthorityResolverService(!environmentService.expectsResolverExtension, connectionToken, resourceUriProvider, this.configuration.serverBasePath, productService, logService); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); // Signing @@ -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/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 17354e5f403..e964af6543e 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -30,6 +30,7 @@ import { registerWindowDriver } from 'vs/workbench/services/driver/browser/drive import { CodeWindow, isAuxiliaryWindow, mainWindow } from 'vs/base/browser/window'; import { createSingleCallFunction } from 'vs/base/common/functional'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export abstract class BaseWindow extends Disposable { @@ -39,7 +40,8 @@ export abstract class BaseWindow extends Disposable { constructor( targetWindow: CodeWindow, dom = { getWindowsCount, getWindows }, /* for testing */ - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); @@ -52,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/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index b2aa088f3f3..57cab7b276f 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -12,6 +12,7 @@ import { isStandalone } from 'vs/base/browser/browser'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { ActivityBarPosition, EditorActionsLocation, EditorTabsMode, LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; import { defaultWindowTitle, defaultWindowTitleSeparator } from 'vs/workbench/browser/parts/titlebar/windowTitle'; +import { CustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; const registry = Registry.as(ConfigurationExtensions.Configuration); @@ -85,6 +86,34 @@ const registry = Registry.as(ConfigurationExtensions.Con 'markdownDescription': localize('decorations.colors', "Controls whether editor file decorations should use colors."), 'default': true }, + [CustomEditorLabelService.SETTING_ID_ENABLED]: { + 'type': 'boolean', + 'markdownDescription': localize('workbench.editor.label.enabled', "Controls whether the custom workbench editor labels should be applied."), + 'default': true, + }, + [CustomEditorLabelService.SETTING_ID_PATTERNS]: { + 'type': 'object', + 'markdownDescription': (() => { + let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:"); + customEditorLabelDescription += '\n- ' + [ + localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `root/folder/file.txt -> folder`)."), + localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=1: root/folder/file.txt -> root`)."), + localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `root/folder/file.txt -> file`)."), + localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `root/folder/file.txt -> txt`)."), + ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations + customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `root/static/folder/file.html` as `file - folder (html)`."); + + return customEditorLabelDescription; + })(), + additionalProperties: + { + type: 'string', + markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern mtches. May include the variables ${dirname}, ${filename} and ${extname}."), + minLength: 1, + pattern: '.*[a-zA-Z0-9].*' + }, + 'default': {} + }, 'workbench.editor.labelFormat': { 'type': 'string', 'enum': ['default', 'short', 'medium', 'long'], @@ -262,12 +291,12 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.editor.enablePreviewFromQuickOpen': { 'type': 'boolean', - 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'enablePreviewFromQuickOpen' }, "Controls whether editors opened from Quick Open show as preview editors. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). When enabled, hold Ctrl before selection to open an editor as a non-preview. This value is ignored when {0} is not set to {1}.", '`#workbench.editor.enablePreview#`', '`multiple`'), + 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'enablePreviewFromQuickOpen' }, "Controls whether editors opened from Quick Open show as preview editors. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). When enabled, hold Ctrl before selection to open an editor as a non-preview. This value is ignored when {0} is not set to {1}.", '`#workbench.editor.showTabs#`', '`multiple`'), 'default': false }, 'workbench.editor.enablePreviewFromCodeNavigation': { 'type': 'boolean', - 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'enablePreviewFromCodeNavigation' }, "Controls whether editors remain in preview when a code navigation is started from them. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). This value is ignored when {0} is not set to {1}.", '`#workbench.editor.enablePreview#`', '`multiple`'), + 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'enablePreviewFromCodeNavigation' }, "Controls whether editors remain in preview when a code navigation is started from them. Preview editors do not stay open, and are reused until explicitly set to be kept open (via double-click or editing). This value is ignored when {0} is not set to {1}.", '`#workbench.editor.showTabs#`', '`multiple`'), 'default': false }, 'workbench.editor.closeOnFileDelete': { @@ -500,13 +529,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': { @@ -813,6 +843,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/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 53dc90bc4b4..c69c214d58c 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/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/src/vs/workbench/common/comments.ts b/src/vs/workbench/common/comments.ts new file mode 100644 index 00000000000..038819d8f99 --- /dev/null +++ b/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/src/vs/workbench/common/component.ts b/src/vs/workbench/common/component.ts index 6c25dc9d977..f8dd0115412 100644 --- a/src/vs/workbench/common/component.ts +++ b/src/vs/workbench/common/component.ts @@ -20,7 +20,6 @@ export class Component extends Themable { ) { super(themeService); - this.id = id; this.memento = new Memento(this.id, storageService); this._register(storageService.onWillSaveState(() => { diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index bf5d6c5feed..dc24565fb5e 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/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/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index b96df678196..aaf1452c25a 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/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/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index c6986bd0f57..bd2c42d8510 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorDescriptor { /** * Instantiates the editor pane using the provided services. */ - instantiate(instantiationService: IInstantiationService): T; + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): T; /** * Whether the descriptor is for the provided editor pane. @@ -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/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 8a018234256..60047f630e3 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/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/src/vs/workbench/common/editor/editorInput.ts b/src/vs/workbench/common/editor/editorInput.ts index 8b80fe942a3..d6a10e8c35e 100644 --- a/src/vs/workbench/common/editor/editorInput.ts +++ b/src/vs/workbench/common/editor/editorInput.ts @@ -288,6 +288,19 @@ export abstract class EditorInput extends AbstractEditorInput { return this; } + /** + * Indicates if this editor can be moved to another group. By default + * editors can freely be moved around groups. If an editor cannot be + * moved, a message should be returned to show to the user. + * + * @returns `true` if the editor can be moved to the target group, or + * a string with a message to show to the user if the editor cannot be + * moved. + */ + canMove(sourceGroup: GroupIdentifier, targetGroup: GroupIdentifier): true | string { + return true; + } + /** * Returns if the other object matches this input. */ diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index 7b427fe5ded..390b19874c8 100644 --- a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/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/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 6f5af0deee3..7bc68f5e366 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -13,6 +13,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IMarkdownString } from 'vs/base/common/htmlContent'; import { isConfigured } from 'vs/platform/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; /** * The base class for all editor inputs that open resources. @@ -46,7 +47,8 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements @ILabelService protected readonly labelService: ILabelService, @IFileService protected readonly fileService: IFileService, @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService, - @ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService + @ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService protected readonly customEditorLabelService: ICustomEditorLabelService ) { super(); @@ -61,6 +63,7 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); + this._register(this.customEditorLabelService.onDidChange(() => this.updateLabel())); } private onLabelEvent(scheme: string): void { @@ -95,7 +98,7 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements private _name: string | undefined = undefined; override getName(): string { if (typeof this._name !== 'string') { - this._name = this.labelService.getUriBasenameLabel(this._preferredResource); + this._name = this.customEditorLabelService.getName(this._preferredResource) ?? this.labelService.getUriBasenameLabel(this._preferredResource); } return this._name; diff --git a/src/vs/workbench/common/editor/sideBySideEditorInput.ts b/src/vs/workbench/common/editor/sideBySideEditorInput.ts index 7228b47ae71..0c6e63d43ce 100644 --- a/src/vs/workbench/common/editor/sideBySideEditorInput.ts +++ b/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/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index 8416d2cc249..9635fa3f670 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -19,6 +19,7 @@ import { IReference } from 'vs/base/common/lifecycle'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; /** * The base class for all editor inputs that open in text editors. @@ -33,9 +34,10 @@ export abstract class AbstractTextResourceEditorInput extends AbstractResourceEd @ILabelService labelService: ILabelService, @IFileService fileService: IFileService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService ) { - super(resource, preferredResource, labelService, fileService, filesConfigurationService, textResourceConfigurationService); + super(resource, preferredResource, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); } override save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { @@ -107,9 +109,10 @@ export class TextResourceEditorInput extends AbstractTextResourceEditorInput imp @IFileService fileService: IFileService, @ILabelService labelService: ILabelService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService ) { - super(resource, undefined, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService); + super(resource, undefined, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); } override getName(): string { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 39dbf22bdfd..8e6aa893cf4 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -733,28 +733,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 --- > @@ -904,6 +918,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/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index e529937c345..766d85c3942 100644 --- a/src/vs/workbench/common/views.ts +++ b/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/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index fb097ebecaa..945637c5dfb 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/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/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 9a42093991c..51e86960f7c 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/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/src/vs/workbench/contrib/accessibility/browser/accessibilityStatus.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityStatus.ts index de8cd7038f8..8e26745dd7a 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityStatus.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityStatus.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, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; @@ -13,34 +13,6 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { INotificationHandle, INotificationService, NotificationPriority } from 'vs/platform/notification/common/notification'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; - -class ScreenReaderModeStatusEntry extends Disposable { - - private readonly screenReaderModeElement = this._register(new MutableDisposable()); - - constructor(@IStatusbarService private readonly statusbarService: IStatusbarService) { - super(); - } - - updateScreenReaderModeElement(visible: boolean): void { - if (visible) { - if (!this.screenReaderModeElement.value) { - const text = localize('screenReaderDetected', "Screen Reader Optimized"); - this.screenReaderModeElement.value = this.statusbarService.addEntry({ - name: localize('status.editor.screenReaderMode', "Screen Reader Mode"), - text, - ariaLabel: text, - command: 'showEditorScreenReaderNotification', - kind: 'prominent' - }, 'status.editor.screenReaderMode', StatusbarAlignment.RIGHT, 100.6); - } - } else { - this.screenReaderModeElement.clear(); - } - } -} export class AccessibilityStatus extends Disposable implements IWorkbenchContribution { @@ -48,40 +20,23 @@ export class AccessibilityStatus extends Disposable implements IWorkbenchContrib private screenReaderNotification: INotificationHandle | null = null; private promptedScreenReader: boolean = false; - private readonly screenReaderModeElements = new Set(); + private readonly screenReaderModeElement = this._register(new MutableDisposable()); constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @INotificationService private readonly notificationService: INotificationService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IInstantiationService instantiationService: IInstantiationService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + @IStatusbarService private readonly statusbarService: IStatusbarService ) { super(); - this.createScreenReaderModeElement(instantiationService, this._store); - this.updateScreenReaderModeElements(accessibilityService.isScreenReaderOptimized()); + this._register(CommandsRegistry.registerCommand({ id: 'showEditorScreenReaderNotification', handler: () => this.showScreenReaderNotification() })); - CommandsRegistry.registerCommand({ id: 'showEditorScreenReaderNotification', handler: () => this.showScreenReaderNotification() }); + this.updateScreenReaderModeElement(this.accessibilityService.isScreenReaderOptimized()); this.registerListeners(); } - private createScreenReaderModeElement(instantiationService: IInstantiationService, disposables: DisposableStore): ScreenReaderModeStatusEntry { - const entry = disposables.add(instantiationService.createInstance(ScreenReaderModeStatusEntry)); - - this.screenReaderModeElements.add(entry); - disposables.add(toDisposable(() => this.screenReaderModeElements.delete(entry))); - - return entry; - } - - private updateScreenReaderModeElements(visible: boolean): void { - for (const entry of this.screenReaderModeElements) { - entry.updateScreenReaderModeElement(visible); - } - } - private registerListeners(): void { this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.onScreenReaderModeChange())); @@ -90,11 +45,6 @@ export class AccessibilityStatus extends Disposable implements IWorkbenchContrib this.onScreenReaderModeChange(); } })); - - this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(({ instantiationService, disposables }) => { - const entry = this.createScreenReaderModeElement(instantiationService, disposables); - entry.updateScreenReaderModeElement(this.accessibilityService.isScreenReaderOptimized()); - })); } private showScreenReaderNotification(): void { @@ -123,6 +73,23 @@ export class AccessibilityStatus extends Disposable implements IWorkbenchContrib Event.once(this.screenReaderNotification.onDidClose)(() => this.screenReaderNotification = null); } + private updateScreenReaderModeElement(visible: boolean): void { + if (visible) { + if (!this.screenReaderModeElement.value) { + const text = localize('screenReaderDetected', "Screen Reader Optimized"); + this.screenReaderModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.screenReaderMode', "Screen Reader Mode"), + text, + ariaLabel: text, + command: 'showEditorScreenReaderNotification', + kind: 'prominent', + showInAllWindows: true + }, 'status.editor.screenReaderMode', StatusbarAlignment.RIGHT, 100.6); + } + } else { + this.screenReaderModeElement.clear(); + } + } private onScreenReaderModeChange(): void { @@ -141,14 +108,6 @@ export class AccessibilityStatus extends Disposable implements IWorkbenchContrib if (this.screenReaderNotification) { this.screenReaderNotification.close(); } - this.updateScreenReaderModeElements(this.accessibilityService.isScreenReaderOptimized()); - } - - override dispose(): void { - super.dispose(); - - for (const entry of this.screenReaderModeElements) { - entry.dispose(); - } + this.updateScreenReaderModeElement(this.accessibilityService.isScreenReaderOptimized()); } } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 5f3a2df9d06..b64f3dc5e62 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -13,12 +13,12 @@ import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; @@ -29,6 +29,7 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewDelegate, IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -39,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 { @@ -90,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 { @@ -126,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; @@ -136,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; @@ -157,7 +171,9 @@ export class AccessibleView extends Disposable { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ILayoutService private readonly _layoutService: ILayoutService, - @IMenuService private readonly _menuService: IMenuService + @IMenuService private readonly _menuService: IMenuService, + @ICommandService private readonly _commandService: ICommandService, + @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService ) { super(); @@ -167,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'); @@ -227,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 { @@ -252,6 +277,19 @@ 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) { return; @@ -302,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 { @@ -325,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; @@ -427,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 = ''; @@ -456,9 +523,11 @@ 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) => { if (!model) { return; @@ -483,6 +552,11 @@ export class AccessibleView extends Disposable { } else if (actionsHint) { ariaLabel = localize('accessibility-help-hint', "Accessibility Help, {0}", actionsHint); } + if (isWindows && widgetIsFocused) { + // prevent the screen reader on windows from reading + // the aria label again when it's refocused + ariaLabel = ''; + } this._editorWidget.updateOptions({ ariaLabel }); this._editorWidget.focus(); if (this._currentProvider?.options.position) { @@ -506,10 +580,13 @@ 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) => { - if (e.keyCode === KeyCode.Escape || shouldHide(e.browserEvent, this._keybindingService, this._configurationService)) { + if (e.keyCode === KeyCode.Enter) { + this._commandService.executeCommand('editor.action.openLink'); + } else if (e.keyCode === KeyCode.Escape || shouldHide(e.browserEvent, this._keybindingService, this._configurationService)) { hide(e); } else if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) { const url: string = provider.options.readMoreUrl; @@ -607,7 +684,8 @@ export class AccessibleView extends Disposable { private _getAccessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { const navigationHint = this._getNavigationHint(); const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); - const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))."); + 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) { @@ -619,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 into New File 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; } @@ -652,7 +761,7 @@ export class AccessibleView extends Disposable { let goToSymbolHint = ''; if (providerHasSymbols) { if (goToSymbolKb) { - goToSymbolHint = localize('goToSymbolHint', 'Go to a symbol ({0})', goToSymbolKb); + goToSymbolHint = localize('goToSymbolHint', 'Go to a symbol ({0}).', goToSymbolKb); } else { goToSymbolHint = localize('goToSymbolHintNoKb', 'To go to a symbol, configure a keybinding for the command Go To Symbol in Accessible View'); } @@ -724,6 +833,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView editorWidget?.revealLine(position.lineNumber); } } + getCodeBlockContext(): ICodeBlockActionContext | undefined { + return this._accessibleView?.getCodeBlockContext(); + } } class AccessibleViewSymbolQuickPick { @@ -765,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/src/vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement.ts b/src/vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement.ts new file mode 100644 index 00000000000..ed3d0f75cb0 --- /dev/null +++ b/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/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts similarity index 73% rename from src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts rename to src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts index 87abfd5cb8d..e4df2fcc74e 100644 --- a/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts +++ b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts @@ -9,17 +9,15 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export class SaveAudioCueContribution extends Disposable implements IWorkbenchContribution { +export class SaveAccessibilitySignalContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.saveAudioCues'; + static readonly ID = 'workbench.contrib.saveAccessibilitySignal'; constructor( @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); - this._register(this._workingCopyService.onDidSave((e) => { - this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }); - })); + this._register(this._workingCopyService.onDidSave(e => this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }))); } } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts index bff29815a40..44a8f0236d0 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts +++ b/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/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts index 329cabe4bf3..ec79963a27f 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts @@ -76,7 +76,7 @@ export class ShowSignalSoundHelp extends Action2 { qp.onDidChangeActive(() => { accessibilitySignalService.playSound(qp.activeItems[0].signal.sound.getSound(true), true); }); - qp.placeholder = localize('audioCues.help.placeholder', 'Select a sound to play and configure'); + qp.placeholder = localize('sounds.help.placeholder', 'Select a sound to play and configure'); qp.canSelectMany = true; await qp.show(); } diff --git a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index ecc7d689e27..4975185248d 100644 --- a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/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); + 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/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts new file mode 100644 index 00000000000..3c3d4b9aee1 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * 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, localize2 } 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, IAuthenticationService } 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: localize2('manageTrustedExtensionsForAccount', "Manage Trusted Extensions For Account"), + category: localize2('accounts', "Accounts"), + f1: true + }); + } + + override async run(accessor: ServicesAccessor, options?: { 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 authenticationService = accessor.get(IAuthenticationService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + + let providerId = options?.providerId; + let accountLabel = options?.accountLabel; + + if (!providerId || !accountLabel) { + const accounts = new Array<{ providerId: string; providerLabel: string; accountLabel: string }>(); + for (const id of authenticationService.getProviderIds()) { + const providerLabel = authenticationService.getProvider(id).label; + const sessions = await authenticationService.getSessions(id); + const uniqueAccountLabels = new Set(); + for (const session of sessions) { + if (!uniqueAccountLabels.has(session.account.label)) { + uniqueAccountLabels.add(session.account.label); + accounts.push({ providerId: id, providerLabel, accountLabel: session.account.label }); + } + } + } + + const pick = await quickInputService.pick( + accounts.map(account => ({ + providerId: account.providerId, + label: account.accountLabel, + description: account.providerLabel + })), + { + placeHolder: localize('pickAccount', "Pick an account to manage trusted extensions for"), + matchOnDescription: true, + } + ); + + if (pick) { + providerId = pick.providerId; + accountLabel = pick.label; + } else { + return; + } + } + + 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): IQuickPickItem & { 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; + let disabled: boolean | undefined; + if (extension.trusted) { + tooltip = localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); + disabled = true; + } + return { + label: extension.name, + extension, + description, + tooltip, + disabled, + picked: extension.allowed === undefined || extension.allowed + }; + }; + 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); + + const allowedExtensionsSet = new Set(quickPick.selectedItems.map(i => i.extension)); + updatedAllowedList.forEach(extension => { + extension.allowed = allowedExtensionsSet.has(extension); + }); + authenticationAccessService.updateAllowedExtensions(providerId, accountLabel, updatedAllowedList); + quickPick.hide(); + })); + + disposableStore.add(quickPick.onDidHide(() => { + disposableStore.dispose(); + })); + + disposableStore.add(quickPick.onDidCustom(() => { + quickPick.hide(); + })); + + quickPick.show(); + } + +} diff --git a/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts new file mode 100644 index 00000000000..87afd379e24 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; + +export class SignOutOfAccountAction extends Action2 { + constructor() { + super({ + id: '_signOutOfAccount', + title: localize('signOutOfAccount', "Sign out of account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const authenticationService = accessor.get(IAuthenticationService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + const dialogService = accessor.get(IDialogService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allSessions = await authenticationService.getSessions(providerId); + const sessions = allSessions.filter(s => s.account.label === accountLabel); + + const accountUsages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + + const { confirmed } = await dialogService.confirm({ + type: Severity.Info, + message: accountUsages.length + ? localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountLabel, accountUsages.map(usage => usage.extensionName).join('\n')) + : localize('signOutMessageSimple', "Sign out of '{0}'?", accountLabel), + primaryButton: localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") + }); + + if (confirmed) { + const removeSessionPromises = sessions.map(session => authenticationService.removeSession(providerId, session.id)); + await Promise.all(removeSessionPromises); + authenticationUsageService.removeAccountUsage(providerId, accountLabel); + authenticationAccessService.removeAllowedExtensions(providerId, accountLabel); + } + } +} diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts new file mode 100644 index 00000000000..36d4e4e1352 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { SignOutOfAccountAction } from 'vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction'; +import { AuthenticationProviderInformation, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ManageTrustedExtensionsForAccountAction } from './actions/manageTrustedExtensionsForAccountAction'; + +const codeExchangeProxyCommand = CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { + const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); + return environmentService.options?.codeExchangeProxyEndpoints; +}); + +const authenticationDefinitionSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + description: localize('authentication.id', 'The id of the authentication provider.') + }, + label: { + type: 'string', + description: localize('authentication.label', 'The human readable name of the authentication provider.'), + } + } +}; + +const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'authentication', + jsonSchema: { + description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), + type: 'array', + items: authenticationDefinitionSchema + }, + activationEventsGenerator: (authenticationProviders, result) => { + for (const authenticationProvider of authenticationProviders) { + if (authenticationProvider.id) { + result.push(`onAuthenticationRequest:${authenticationProvider.id}`); + } + } + } +}); + +class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + + readonly type = 'table'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.authentication; + } + + render(manifest: IExtensionManifest): IRenderedData { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('authenticationlabel', "Label"), + localize('authenticationid', "ID"), + ]; + + const rows: IRowData[][] = authentication + .sort((a, b) => a.label.localeCompare(b.label)) + .map(auth => { + return [ + auth.label, + auth.id, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +const extensionFeature = Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'authentication', + label: localize('authentication', "Authentication"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(AuthenticationDataRenderer), +}); + +export class AuthenticationContribution extends Disposable implements IWorkbenchContribution { + static ID = 'workbench.contrib.authentication'; + + private _placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('authentication.Placeholder', "No accounts requested yet..."), + precondition: ContextKeyExpr.false() + }, + }); + + constructor( + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService + ) { + super(); + this._register(codeExchangeProxyCommand); + this._register(extensionFeature); + + this._registerHandlers(); + this._registerAuthenticationExtentionPointHandler(); + this._registerEnvContributedAuthenticationProviders(); + this._registerActions(); + } + + private _registerAuthenticationExtentionPointHandler(): void { + authenticationExtPoint.setHandler((extensions, { added, removed }) => { + added.forEach(point => { + for (const provider of point.value) { + if (isFalsyOrWhitespace(provider.id)) { + point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + continue; + } + + if (isFalsyOrWhitespace(provider.label)) { + point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + continue; + } + + if (!this._authenticationService.declaredProviders.some(p => p.id === provider.id)) { + this._authenticationService.registerDeclaredAuthenticationProvider(provider); + } else { + point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + } + }); + + const removedExtPoints = removed.flatMap(r => r.value); + removedExtPoints.forEach(point => { + const provider = this._authenticationService.declaredProviders.find(provider => provider.id === point.id); + if (provider) { + this._authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + } + }); + }); + } + + private _registerEnvContributedAuthenticationProviders(): void { + if (!this._environmentService.options?.authenticationProviders?.length) { + return; + } + for (const provider of this._environmentService.options.authenticationProviders) { + this._authenticationService.registerAuthenticationProvider(provider.id, provider); + } + } + + private _registerHandlers(): void { + this._register(this._authenticationService.onDidRegisterAuthenticationProvider(_e => { + this._placeholderMenuItem?.dispose(); + this._placeholderMenuItem = undefined; + })); + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(_e => { + if (!this._authenticationService.getProviderIds().length) { + this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('loading', "Loading..."), + precondition: ContextKeyExpr.false() + } + }); + } + })); + } + + private _registerActions(): void { + this._register(registerAction2(SignOutOfAccountAction)); + this._register(registerAction2(ManageTrustedExtensionsForAccountAction)); + } +} + +registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts index c3131b30d44..7e97fd01a69 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts @@ -178,7 +178,7 @@ registerAction2(class ApplyAction extends Action2 { keybinding: { weight: KeybindingWeight.EditorContrib - 10, when: ContextKeyExpr.and(BulkEditPreviewContribution.ctxEnabled, FocusedViewContext.isEqualTo(BulkEditPane.ID)), - primary: KeyMod.Shift + KeyCode.Enter, + primary: KeyMod.CtrlCmd + KeyCode.Enter, } }); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 278de48c47c..8c89ac691ee 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -37,8 +37,9 @@ import { ButtonBar } from 'vs/base/browser/ui/button/button'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Mutable } from 'vs/base/common/types'; import { IResourceDiffEditorInput } from 'vs/workbench/common/editor'; -import { IMultiDiffEditorOptions } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl'; +import { IMultiDiffEditorOptions, IMultiDiffResourceId } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; import { IRange } from 'vs/editor/common/core/range'; +import { CachedFunction, LRUCachedFunction } from 'vs/base/common/cache'; const enum State { Data = 'data', @@ -70,8 +71,6 @@ export class BulkEditPane extends ViewPane { private _currentResolve?: (edit?: ResourceEdit[]) => void; private _currentInput?: BulkFileOperations; private _currentProvider?: BulkEditPreviewProvider; - private _fileOperations?: BulkFileOperation[]; - private _resources?: IResourceDiffEditorInput[]; constructor( options: IViewletViewOptions, @@ -100,6 +99,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 { @@ -339,12 +344,13 @@ export class BulkEditPane extends ViewPane { return; } - const resources = await this._resolveResources(fileOperations); + const result = await this._computeResourceDiffEditorInputs.get(fileOperations); + const resourceId = await result.getResourceDiffEditorInputIdOfOperation(fileElement.edit); const options: Mutable = { ...e.editorOptions, viewState: { revealData: { - resource: { original: fileElement.edit.uri }, + resource: resourceId, range: selection, } } @@ -353,49 +359,56 @@ export class BulkEditPane extends ViewPane { const label = 'Refactor Preview'; this._editorService.openEditor({ multiDiffSource, - resources, label, options, isTransient: true, - description: label + description: label, + resources: result.resources }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } - private async _resolveResources(fileOperations: BulkFileOperation[]): Promise { - if (this._fileOperations === fileOperations && this._resources) { - return this._resources; - } - const sortedFileOperations = fileOperations.sort(compareBulkFileOperations); - const resources: IResourceDiffEditorInput[] = []; - for (const operation of sortedFileOperations) { - const operationUri = operation.uri; - const previewUri = this._currentProvider!.asPreviewUri(operationUri); - // delete -> show single editor - if (operation.type & BulkFileOperationType.Delete) { - resources.push({ - original: { resource: undefined }, - modified: { resource: URI.revive(previewUri) } - }); + private readonly _computeResourceDiffEditorInputs = new LRUCachedFunction(async (fileOperations: BulkFileOperation[]) => { + const computeDiffEditorInput = new CachedFunction>(async (fileOperation) => { + const fileOperationUri = fileOperation.uri; + const previewUri = this._currentProvider!.asPreviewUri(fileOperationUri); + // delete + if (fileOperation.type & BulkFileOperationType.Delete) { + return { + original: { resource: URI.revive(previewUri) }, + modified: { resource: undefined } + }; - } else { - // rename, create, edits -> show diff editr + } + // rename, create, edits + else { let leftResource: URI | undefined; try { - (await this._textModelService.createModelReference(operationUri)).dispose(); - leftResource = operationUri; + (await this._textModelService.createModelReference(fileOperationUri)).dispose(); + leftResource = fileOperationUri; } catch { leftResource = BulkEditPreviewProvider.emptyPreview; } - resources.push({ + return { original: { resource: URI.revive(leftResource) }, modified: { resource: URI.revive(previewUri) } - }); + }; } + }); + + const sortedFileOperations = fileOperations.slice().sort(compareBulkFileOperations); + const resources: IResourceDiffEditorInput[] = []; + for (const operation of sortedFileOperations) { + resources.push(await computeDiffEditorInput.get(operation)); } - this._fileOperations = fileOperations; - this._resources = resources; - return resources; - } + const getResourceDiffEditorInputIdOfOperation = async (operation: BulkFileOperation): Promise => { + const resource = await computeDiffEditorInput.get(operation); + return { original: resource.original.resource, modified: resource.modified.resource }; + }; + return { + resources, + getResourceDiffEditorInputIdOfOperation + }; + }, key => key); private _onContextMenu(e: ITreeContextMenuEvent): void { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index c5fd28d6a8e..e45a95008c3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/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/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts index 76497826d74..22c507ca0b3 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts +++ b/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/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index 6eb9f63cba9..3dada0c5698 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -19,7 +19,7 @@ import { SplitView, Orientation, Sizing } from 'vs/base/browser/ui/splitview/spl import { Dimension, isKeyboardEvent } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 9fd67d69b88..e225605c017 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -9,10 +9,10 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; 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'; +import { INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string { const keybindingService = accessor.get(IKeybindingService); @@ -81,10 +81,12 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi if (type === 'panelChat' && cachedPosition) { inputEditor.setPosition(cachedPosition); inputEditor.focus(); + } else if (type === 'inlineChat') { - if (editor) { - InlineChatController.get(editor)?.focus(); - } + // TODO@jrieken find a better way for this + const ctrl = <{ focus(): void } | undefined>editor?.getContribution(INLINE_CHAT_ID); + ctrl?.focus(); + } }, options: { type: AccessibleViewType.Help } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 929fb17898c..81f5f65e1bc 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -10,16 +10,15 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IsLinuxContext, IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; @@ -27,14 +26,14 @@ import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS, CONTEXT_REQUEST, CONTEXT_RESPONSE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS, CONTEXT_REQUEST, CONTEXT_RESPONSE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { chatAgentLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IsLinuxContext, IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export const CHAT_CATEGORY = localize2('chat.category', 'Chat'); export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open'; @@ -101,11 +100,16 @@ export class ChatSubmitSecondaryAgentEditorAction extends EditorAction2 { super({ id: ChatSubmitSecondaryAgentEditorAction.ID, title: localize2({ key: 'actions.chat.submitSecondaryAgent', comment: ['Send input from the chat input box to the secondary agent'] }, "Submit to Secondary Agent"), - precondition: CONTEXT_IN_CHAT_INPUT, + precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_INPUT_HAS_AGENT.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), keybinding: { - when: EditorContextKeys.textInputFocus, + when: CONTEXT_IN_CHAT_INPUT, primary: KeyMod.CtrlCmd | KeyCode.Enter, weight: KeybindingWeight.EditorContrib + }, + menu: { + id: MenuId.ChatExecuteSecondary, + group: 'group_1', + when: CONTEXT_CHAT_INPUT_HAS_AGENT.negate(), } }); } @@ -128,7 +132,7 @@ export class ChatSubmitSecondaryAgentEditorAction extends EditorAction2 { if (widget.getInput().match(/^\s*@/)) { widget.acceptInput(); } else { - widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.id}`); + widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.name}`); } } } @@ -141,12 +145,17 @@ export class ChatSubmitEditorAction extends EditorAction2 { super({ id: ChatSubmitEditorAction.ID, title: localize2({ key: 'actions.chat.submit', comment: ['Apply input from the chat input box'] }, "Submit"), - precondition: CONTEXT_IN_CHAT_INPUT, + precondition: CONTEXT_CHAT_INPUT_HAS_TEXT, keybinding: { - when: EditorContextKeys.textInputFocus, + when: CONTEXT_IN_CHAT_INPUT, primary: KeyCode.Enter, weight: KeybindingWeight.EditorContrib - } + }, + menu: { + id: MenuId.ChatExecuteSecondary, + when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), + group: 'group_1', + }, }); } @@ -295,7 +304,7 @@ const getHistoryChatActionDescriptorForViewTitle = (viewId: string, providerId: }, category: CHAT_CATEGORY, icon: Codicon.history, - f1: false, + f1: true, precondition: CONTEXT_PROVIDER_EXISTS }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 27f5c9f07f8..3cd6e415c9c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -24,13 +24,13 @@ 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'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { CTX_INLINE_CHAT_VISIBLE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -87,7 +87,7 @@ export function registerChatCodeBlockActions() { icon: Codicon.copy, menu: { id: MenuId.ChatCodeBlock, - group: 'navigation', + group: 'navigation' } }); } @@ -147,21 +147,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,13 +187,13 @@ 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 + weight: KeybindingWeight.ExternalExtension + 1 }, }); } @@ -351,7 +354,7 @@ export function registerChatCodeBlockActions() { menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - isHiddenByDefault: true, + isHiddenByDefault: true } }); } @@ -419,11 +422,6 @@ export function registerChatCodeBlockActions() { CONTEXT_IN_CHAT_SESSION, ...shellLangIds.map(e => ContextKeyExpr.notEquals(EditorContextKeys.languageId.key, e)) ) - }, - { - id: MenuId.ChatCodeBlock, - group: 'navigation', - when: CTX_INLINE_CHAT_VISIBLE, }], keybinding: [{ primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter, @@ -431,7 +429,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 +555,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/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 9ceec5ff1d1..4888f64fa5a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; export interface IVoiceChatExecuteActionContext { @@ -32,7 +35,7 @@ export class SubmitAction extends Action2 { f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: CONTEXT_CHAT_INPUT_HAS_TEXT, + precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), menu: { id: MenuId.ChatExecute, when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), @@ -50,34 +53,72 @@ export class SubmitAction extends Action2 { } } -export function registerChatExecuteActions() { - registerAction2(SubmitAction); - registerAction2(class CancelAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.cancel', - title: localize2('interactive.cancel.label', "Cancel"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.debugStop, - menu: { - id: MenuId.ChatExecute, - when: CONTEXT_CHAT_REQUEST_IN_PROGRESS, - group: 'navigation', - } - }); +class SendToNewChatAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.sendToNewChat', + title: localize2('chat.newChat.label', "Send to New Chat"), + precondition: ContextKeyExpr.and(CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CONTEXT_CHAT_INPUT_HAS_TEXT), + category: CHAT_CATEGORY, + f1: false, + menu: { + id: MenuId.ChatExecuteSecondary, + group: 'group_2' + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, + when: CONTEXT_IN_CHAT_INPUT, + } + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const context: IChatExecuteActionContext | undefined = args[0]; + + const widgetService = accessor.get(IChatWidgetService); + const widget = context?.widget ?? widgetService.lastFocusedWidget; + if (!widget) { + return; } - run(accessor: ServicesAccessor, ...args: any[]) { - const context: IChatExecuteActionContext = args[0]; - if (!context.widget) { - return; - } + widget.clear(); + widget.acceptInput(context?.inputValue); + } +} - const chatService = accessor.get(IChatService); - if (context.widget.viewModel) { - chatService.cancelCurrentRequestForSession(context.widget.viewModel.sessionId); +export class CancelAction extends Action2 { + static readonly ID = 'workbench.action.chat.cancel'; + constructor() { + super({ + id: CancelAction.ID, + title: localize2('interactive.cancel.label', "Cancel"), + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.debugStop, + menu: { + id: MenuId.ChatExecute, + when: CONTEXT_CHAT_REQUEST_IN_PROGRESS, + group: 'navigation', } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const context: IChatExecuteActionContext = args[0]; + if (!context.widget) { + return; } - }); + + const chatService = accessor.get(IChatService); + if (context.widget.viewModel) { + chatService.cancelCurrentRequestForSession(context.widget.viewModel.sessionId); + } + } +} + +export function registerChatExecuteActions() { + registerAction2(SubmitAction); + registerAction2(CancelAction); + registerAction2(SendToNewChatAction); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts index 4df9285764e..d1863ef7d6c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts @@ -178,7 +178,10 @@ export function getQuickChatActionForProvider(id: string, label: string) { override run(accessor: ServicesAccessor, query?: string): void { const quickChatService = accessor.get(IQuickChatService); - quickChatService.toggle(id, query ? { query } : undefined); + quickChatService.toggle(id, query ? { + query, + selection: new Selection(1, query.length + 1, 1, query.length + 1) + } : undefined); } }; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d1cdc123a6b..85c687b0cff 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/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 { ChatAgentLocation, 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 { ChatProviderService, IChatProviderService } from 'vs/workbench/contrib/chat/common/chatProvider'; -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 + }, } }); @@ -245,7 +250,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { sortText: 'z1_help', executeImmediately: true }, async (prompt, progress) => { - const defaultAgent = chatAgentService.getDefaultAgent(); + const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); const agents = chatAgentService.getAgents(); // Report prefix @@ -262,12 +267,11 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const agentText = (await Promise.all(agents .filter(a => a.id !== defaultAgent?.id) .map(async a => { - const agentWithLeader = `${chatAgentLeader}${a.id}`; + const agentWithLeader = `${chatAgentLeader}${a.name}`; 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 agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.description}`; + 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}`; @@ -328,8 +332,9 @@ registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delay registerSingleton(IQuickChatService, QuickChatService, InstantiationType.Delayed); registerSingleton(IChatAccessibilityService, ChatAccessibilityService, InstantiationType.Delayed); registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed); -registerSingleton(IChatProviderService, ChatProviderService, InstantiationType.Delayed); +registerSingleton(ILanguageModelsService, LanguageModelsService, InstantiationType.Delayed); registerSingleton(IChatSlashCommandService, ChatSlashCommandService, InstantiationType.Delayed); registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); +registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index daa5ac41eb1..adfb6c0b6d9 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -4,18 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 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 { MenuId } from 'vs/platform/actions/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatAgentLocation, 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 +38,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 +66,7 @@ export interface IQuickChatOpenOptions { selection?: Selection; } +export const IChatAccessibilityService = createDecorator('chatAccessibilityService'); export interface IChatAccessibilityService { readonly _serviceBrand: undefined; acceptRequest(): number; @@ -87,6 +91,15 @@ export interface IChatWidgetViewOptions { renderInputOnTop?: boolean; renderStyle?: 'default' | 'compact'; supportsFileReferences?: boolean; + filter?: (item: ChatTreeItem) => boolean; + editableCodeBlocks?: boolean; + menus?: { + executeToolbar?: MenuId; + inputSideToolbar?: MenuId; + telemetrySource?: string; + }; + defaultElementHeight?: number; + editorOverflowWidgetsDomNode?: HTMLElement; } export interface IChatViewViewContext { @@ -103,12 +116,15 @@ export interface IChatWidget { readonly onDidChangeViewModel: Event; readonly onDidAcceptInput: Event; readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; + readonly onDidChangeParsedInput: Event; + readonly location: ChatAgentLocation; readonly viewContext: IChatWidgetViewContext; readonly viewModel: IChatViewModel | undefined; readonly inputEditor: ICodeEditor; readonly providerId: string; readonly supportsFileReferences: boolean; readonly parsedInput: IParsedChatRequest; + lastSelectedAgent: IChatAgentData | undefined; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; @@ -134,3 +150,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/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts new file mode 100644 index 00000000000..933a8940869 --- /dev/null +++ b/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/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index b6c66797247..73a0ebd76a9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -15,7 +15,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi declare readonly _serviceBrand: undefined; - private _pendingCueMap: DisposableMap = this._register(new DisposableMap()); + private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); private _requestId: number = 0; @@ -25,11 +25,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi acceptRequest(): number { this._requestId++; this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); - this._pendingCueMap.set(this._requestId, this._instantiationService.createInstance(AudioCueScheduler)); + this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilitySignalScheduler)); return this._requestId; } acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { - this._pendingCueMap.deleteAndDispose(requestId); + this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.asString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); @@ -46,19 +46,19 @@ const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; /** * Schedules an audio cue to play when a chat response is pending for too long. */ -class AudioCueScheduler extends Disposable { +class AccessibilitySignalScheduler extends Disposable { private _scheduler: RunOnceScheduler; - private _audioCueLoop: IDisposable | undefined; + private _signalLoop: IDisposable | undefined; constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { super(); this._scheduler = new RunOnceScheduler(() => { - this._audioCueLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); + this._signalLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS); this._scheduler.schedule(); } override dispose(): void { super.dispose(); - this._audioCueLoop?.dispose(); + this._signalLoop?.dispose(); this._scheduler.cancel(); this._scheduler.dispose(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 8498292395a..ea834161ce2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -3,12 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, 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 { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; 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 +22,11 @@ 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 { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatContributionService, 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 +64,161 @@ 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', 'id'], + properties: { + id: { + description: localize('chatParticipantId', "A unique id for this chat participant."), + type: 'string' + }, + name: { + description: localize('chatParticipantName', "User-facing display name for this chat participant. The user will use '@' with this name to invoke the 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', + }, + isSticky: { + description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), + 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('chatCommandWhen', "A condition which must be true to enable this command."), + type: 'string' + }, + sampleRequest: { + description: localize('chatCommandSampleRequest', "When the user clicks this command in `/help`, this text will be submitted to this participant."), + type: 'string' + }, + isSticky: { + description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), + 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.id}`); + } + }, +}); + 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(); + private _participantRegistrationDisposables = new DisposableMap(); constructor( - @IChatContributionService readonly _chatContributionService: IChatContributionService + @IChatContributionService private readonly _chatContributionService: IChatContributionService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IProductService private readonly productService: IProductService, + @IContextKeyService private readonly contextService: IContextKeyService, + @ILogService private 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 +242,53 @@ 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; + } + + if (!providerDescriptor.id || !providerDescriptor.name) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`); + continue; + } + + this._participantRegistrationDisposables.set( + getParticipantKey(extension.description.identifier, providerDescriptor.name), + this._chatAgentService.registerAgent( + providerDescriptor.id, + { + extensionId: extension.description.identifier, + id: providerDescriptor.id, + description: providerDescriptor.description, + metadata: { + isSticky: providerDescriptor.isSticky, + }, + name: providerDescriptor.name, + isDefault: providerDescriptor.isDefault, + defaultImplicitVariables: providerDescriptor.defaultImplicitVariables, + locations: isNonEmptyArray(providerDescriptor.locations) ? + providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : + [ChatAgentLocation.Panel], + slashCommands: providerDescriptor.commands ?? [] + } satisfies IChatAgentData)); + } + } + + for (const extension of delta.removed) { + for (const providerDescriptor of extension.value) { + this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.name)); + } + } + }); } private registerViewContainer(): ViewContainer { @@ -123,6 +316,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,6 +350,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { + return `${extensionId.value}_${participantName}`; +} + export class ChatContributionService implements IChatContributionService { declare _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 4651778b165..34f10a76088 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -20,6 +20,8 @@ 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'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; export interface IChatEditorOptions extends IEditorOptions { target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData }; @@ -37,13 +39,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() { @@ -57,6 +60,7 @@ export class ChatEditor extends EditorPane { this.widget = this._register( scopedInstantiationService.createInstance( ChatWidget, + ChatAgentLocation.Panel, { resource: true }, { supportsFileReferences: true }, { diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index 7f0c4279c5d..29a5ba75b7e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -9,7 +9,7 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatFollowup } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; @@ -20,6 +20,7 @@ export class ChatFollowups extend constructor( container: HTMLElement, followups: T[], + private readonly location: ChatAgentLocation, private readonly options: IButtonStyles | undefined, private readonly clickHandler: (followup: T) => void, @IContextKeyService private readonly contextService: IContextKeyService, @@ -37,14 +38,20 @@ export class ChatFollowups extend return; } - if (!this.chatAgentService.getDefaultAgent()) { + if (!this.chatAgentService.getDefaultAgent(this.location)) { // No default agent yet, which affects how followups are rendered, so can't render this yet return; } let tooltipPrefix = ''; - if ('agentId' in followup && followup.agentId && followup.agentId !== this.chatAgentService.getDefaultAgent()?.id) { - tooltipPrefix += `${chatAgentLeader}${followup.agentId} `; + if ('agentId' in followup && followup.agentId && followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { + const agent = this.chatAgentService.getAgent(followup.agentId); + if (!agent) { + // Refers to agent that doesn't exist + return; + } + + tooltipPrefix += `${chatAgentLeader}${agent.name} `; if ('subCommand' in followup && followup.subCommand) { tooltipPrefix += `${chatSubcommandLeader}${followup.subCommand} `; } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f4692878598..f11c2850463 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -4,50 +4,70 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; 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 { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; import { HistoryNavigator } from 'vs/base/common/history'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +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'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; 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 { INotificationService } from 'vs/platform/notification/common/notification'; +import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { asCssVariableWithDefault, checkboxBorder, inputBackground } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { IChatExecuteActionContext, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { ChatSubmitSecondaryAgentEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CancelAction, 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'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { chatAgentLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; 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.$; const INPUT_EDITOR_MAX_HEIGHT = 250; +interface IChatInputPartOptions { + renderFollowups: boolean; + renderStyle?: 'default' | 'compact'; + menus: { + executeToolbar: MenuId; + inputSideToolbar?: MenuId; + telemetrySource?: string; + }; + editorOverflowWidgetsDomNode?: HTMLElement; +} + export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { static readonly INPUT_SCHEME = 'chatSessionInput'; private static _counter = 0; @@ -70,9 +90,24 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private inputEditorHeight = 0; private container!: HTMLElement; + private inputSideToolbarContainer?: HTMLElement; + 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; @@ -90,6 +125,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private inputModel: ITextModel | undefined; private inputEditorHasText: IContextKey; private chatCursorAtTop: IContextKey; + private inputEditorHasFocus: IContextKey; private providerId: string | undefined; private cachedDimensions: dom.Dimension | undefined; @@ -99,26 +135,34 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used - private readonly options: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' }, + private readonly location: ChatAgentLocation, + private readonly options: IChatInputPartOptions, @IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService, @IModelService private readonly modelService: IModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { super(); this.inputEditorHasText = CONTEXT_CHAT_INPUT_HAS_TEXT.bindTo(contextKeyService); this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService); + this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService); 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'); + } })); } @@ -199,7 +243,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge */ async acceptInput(userQuery?: string, inputState?: any): Promise { if (userQuery) { - this.history.add({ text: userQuery, state: inputState }); + let element = this.history.getHistory().find(candidate => candidate.text === userQuery); + if (!element) { + element = { text: userQuery, state: inputState }; + } else { + element.state = inputState; + } + this.history.add(element); } if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) { @@ -225,8 +275,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge render(container: HTMLElement, initialValue: string, widget: IChatWidget) { this.container = dom.append(container, $('.interactive-input-part')); + this.container.classList.toggle('compact', this.options.renderStyle === 'compact'); 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')); @@ -238,7 +291,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement; - const options = getSimpleEditorOptions(this.configurationService); + const options: IEditorConstructionOptions = getSimpleEditorOptions(this.configurationService); + options.overflowWidgetsDomNode = this.options.editorOverflowWidgetsDomNode; options.readOnly = false; options.ariaLabel = this._getAriaLabel(); options.fontFamily = DEFAULT_FONT_FAMILY; @@ -272,7 +326,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Only allow history navigation when the input is empty. // (If this model change happened as a result of a history navigation, this is canceled out by a call in this.navigateHistory) const model = this._inputEditor.getModel(); - const inputHasText = !!model && model.getValueLength() > 0; + const inputHasText = !!model && model.getValue().trim().length > 0; this.inputEditorHasText.set(inputHasText); // If the user is typing on a history entry, then reset the onHistoryEntry flag so that history navigation can be disabled @@ -286,10 +340,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } })); this._register(this._inputEditor.onDidFocusEditorText(() => { + this.inputEditorHasFocus.set(true); this._onDidFocus.fire(); inputContainer.classList.toggle('focused', true); })); this._register(this._inputEditor.onDidBlurEditorText(() => { + this.inputEditorHasFocus.set(false); inputContainer.classList.toggle('focused', false); this._onDidBlur.fire(); @@ -309,14 +365,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } })); - this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputContainer, MenuId.ChatExecute, { + this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputContainer, this.options.menus.executeToolbar, { + telemetrySource: this.options.menus.telemetrySource, menuOptions: { shouldForwardArgs: true }, hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu actionViewItemProvider: (action, options) => { - if (action.id === SubmitAction.ID) { - return this.instantiationService.createInstance(SubmitButtonActionViewItem, { widget } satisfies IChatExecuteActionContext, action, options); + if (this.location === ChatAgentLocation.Panel) { + if ((action.id === SubmitAction.ID || action.id === CancelAction.ID) && action instanceof MenuItemAction) { + const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined); + return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction); + } } return undefined; @@ -330,17 +390,25 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } })); - if (this.options.renderStyle === 'compact') { - const toolbarSide = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputAndSideToolbar, MenuId.ChatInputSide, { + if (this.options.menus.inputSideToolbar) { + const toolbarSide = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputAndSideToolbar, this.options.menus.inputSideToolbar, { + telemetrySource: this.options.menus.telemetrySource, menuOptions: { shouldForwardArgs: true } })); + this.inputSideToolbarContainer = toolbarSide.getElement(); toolbarSide.getElement().classList.add('chat-side-toolbar'); toolbarSide.context = { widget } satisfies IChatExecuteActionContext; } - this.inputModel = this.modelService.getModel(this.inputUri) || this.modelService.createModel('', null, this.inputUri, true); + let inputModel = this.modelService.getModel(this.inputUri); + if (!inputModel) { + inputModel = this.modelService.createModel('', null, this.inputUri, true); + this._register(inputModel); + } + + this.inputModel = inputModel; this.inputModel.updateOptions({ bracketColorizationOptions: { enabled: false, independentColorPoolPerBracketType: false } }); this._inputEditor.setModel(this.inputModel); if (initialValue) { @@ -350,6 +418,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; @@ -358,41 +438,49 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.clearNode(this.followupsContainer); if (items && items.length > 0) { - this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); + this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); } } - 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 inputPartBorder = 0; + const inputPartHorizontalPadding = this.options.renderStyle === 'compact' ? 8 : 40; + const inputPartVerticalPadding = this.options.renderStyle === 'compact' ? 12 : 24; + const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), height - followupsHeight - inputPartVerticalPadding - 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 toolbarPadding = 4; + const sideToolbarWidth = this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); - this._inputEditor.layout({ width: width - inputPartHorizontalPadding - editorBorder - editorPadding - executeToolbarWidth - sideToolbarWidth, height: inputEditorHeight }); + const newEditorWidth = width - inputPartHorizontalPadding - editorBorder - editorPadding - executeToolbarWidth - sideToolbarWidth - toolbarPadding; + const newDimension = { width: newEditorWidth, 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 { @@ -401,40 +489,57 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } -class SubmitButtonActionViewItem extends ActionViewItem { - private readonly _tooltip: string; +function getLastPosition(model: ITextModel): IPosition { + return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 }; +} +// This does seems like a lot just to customize an item with dropdown. This whole class exists just because we need an +// onDidChange listener on the submenu, which is apparently not needed in other cases. +class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { constructor( - context: unknown, - action: IAction, - options: IActionViewItemOptions, - @IKeybindingService keybindingService: IKeybindingService, + action: MenuItemAction, + dropdownAction: IAction, + @IMenuService menuService: IMenuService, + @IContextMenuService contextMenuService: IContextMenuService, @IChatAgentService chatAgentService: IChatAgentService, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + @IThemeService themeService: IThemeService, + @IAccessibilityService accessibilityService: IAccessibilityService ) { - super(context, action, options); - - const primaryKeybinding = keybindingService.lookupKeybinding(ChatSubmitEditorAction.ID)?.getLabel(); - let tooltip = action.label; - if (primaryKeybinding) { - tooltip += ` (${primaryKeybinding})`; - } - - const secondaryAgent = chatAgentService.getSecondaryAgent(); - if (secondaryAgent) { - const secondaryKeybinding = keybindingService.lookupKeybinding(ChatSubmitSecondaryAgentEditorAction.ID)?.getLabel(); - if (secondaryKeybinding) { - tooltip += `\n${chatAgentLeader}${secondaryAgent.id} (${secondaryKeybinding})`; + super( + action, + dropdownAction, + [], + '', + contextMenuService, + { + getKeyBinding: (action: IAction) => keybindingService.lookupKeybinding(action.id, contextKeyService) + }, + keybindingService, + notificationService, + contextKeyService, + themeService, + accessibilityService); + const menu = menuService.createMenu(MenuId.ChatExecuteSecondary, contextKeyService); + const setActions = () => { + const secondary: IAction[] = []; + createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, secondary); + const secondaryAgent = chatAgentService.getSecondaryAgent(); + if (secondaryAgent) { + secondary.forEach(a => { + if (a.id === ChatSubmitSecondaryAgentEditorAction.ID) { + a.label = localize('chat.submitToSecondaryAgent', "Send to @{0}", secondaryAgent.name); + } + + return a; + }); } - } - this._tooltip = tooltip; - } - - protected override getTooltip(): string | undefined { - return this._tooltip; + this.update(dropdownAction, secondary); + }; + setActions(); + this._register(menu.onDidChange(() => setActions())); } } - -function getLastPosition(model: ITextModel): IPosition { - return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 }; -} diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index c0f1827014a..696aa42bf77 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/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'; @@ -23,25 +22,23 @@ 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 { 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 { basenameOrAuthority } from 'vs/base/common/resources'; 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 { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; 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,22 +49,23 @@ 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, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } 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 } 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 { 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 { ChatCodeBlockContentProvider, CodeBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; 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 { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; +import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; const $ = dom.$; @@ -102,6 +100,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 +132,31 @@ 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 +169,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer submenu.actions.length <= 1 + }, actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof MenuItemAction && (action.item.id === 'workbench.action.chat.voteDown' || action.item.id === 'workbench.action.chat.voteUp')) { return scopedInstantiationService.createInstance(ChatVoteButton, action, options as IMenuEntryActionViewItemOptions); } - - return undefined; + return createActionViewItem(scopedInstantiationService, action, options); } })); } @@ -306,6 +295,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIconUri).toString(true); + avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); } else { const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(defaultIcon)); + const icon = element.avatarIcon ?? defaultIcon; + const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } - if (isResponseVM(element) && element.agent && !element.agent.metadata.isDefault) { + if (isResponseVM(element) && element.agent && !element.agent.isDefault) { dom.show(templateData.agentAvatarContainer); const icon = this.getAgentIcon(element.agent.metadata); if (icon instanceof URI) { @@ -481,6 +487,19 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer 0) { + madeChanges = true; + break; + } + } + if (madeChanges) { + dom.append(templateData.value, $('.interactive-edits-summary', undefined, localize('editsSummary', "Made changes."))); + } + } + const newHeight = templateData.rowContainer.offsetHeight; const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; element.currentRenderedHeight = newHeight; @@ -495,6 +514,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickFollowup.fire(followup))); } else { @@ -754,7 +775,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { if (e.element) { - this.openerService.open( - 'uri' in e.element.reference ? e.element.reference.uri : e.element.reference, - { - fromUserGesture: true, - editorOptions: { - ...e.editorOptions, - ...{ - selection: 'range' in e.element.reference ? e.element.reference.range : undefined + const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; + const uri = URI.isUri(uriOrLocation) ? uriOrLocation : + uriOrLocation?.uri; + if (uri) { + this.openerService.open( + uri, + { + fromUserGesture: true, + editorOptions: { + ...e.editorOptions, + ...{ + selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined + } } - } - }); + }); + } } })); listDisposables.add(list.onContextMenu((e) => { @@ -858,7 +884,6 @@ 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).then(ref => ref.object); } 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 modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); + vulns = modelEntry.vulns; + textModel = modelEntry.model; } - 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 }, text); // 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 +935,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; @@ -932,10 +971,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - const ref = this._editorPool.get(data); + private renderCodeBlock(data: ICodeBlockData, text: string): IDisposableReference { + const ref = this._editorPool.get(); const editorInfo = ref.object; - editorInfo.render(data, this._currentLayoutWidth); + if (isResponseVM(data.element)) { + this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + } + + editorInfo.render(data, this._currentLayoutWidth, this.rendererOptions.editableCodeBlock); return ref; } @@ -969,6 +1012,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { constructor( + private readonly defaultElementHeight: number, @ILogService private readonly logService: ILogService ) { } @@ -982,7 +1026,7 @@ export class ChatListDelegate implements IListVirtualDelegate { getHeight(element: ChatTreeItem): number { const kind = isRequestVM(element) ? 'request' : 'response'; - const height = ('currentRenderedHeight' in element ? element.currentRenderedHeight : undefined) ?? 200; + const height = ('currentRenderedHeight' in element ? element.currentRenderedHeight : undefined) ?? this.defaultElementHeight; this._traceLayout('getHeight', `${kind}, height=${height}`); return height; } @@ -996,72 +1040,6 @@ 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; @@ -1070,39 +1048,26 @@ interface IDisposableReference extends IDisposable { 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 +1075,7 @@ class EditorPool extends Disposable { dispose: () => { codeBlock.reset(); stale = true; - pool.release(codeBlock); + this._pool.release(codeBlock); } }; } @@ -1137,7 +1102,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 +1162,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, @@ -1209,10 +1174,13 @@ class ContentReferencesListPool extends Disposable { alwaysConsumeMouseWheel: false, accessibilityProvider: { getAriaLabel: (element: IChatContentReference) => { - if (URI.isUri(element.reference)) { - return basename(element.reference.path); + const reference = element.reference; + if ('variableName' in reference) { + return reference.variableName; + } else if (URI.isUri(reference)) { + return basename(reference.path); } else { - return basename(element.reference.uri.path); + return basename(reference.uri.path); } }, @@ -1258,6 +1226,7 @@ class ContentReferencesListRenderer implements IListRenderer d.value).find((d): d is URI => d instanceof URI) || undefined; - const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : ''; + const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : + part instanceof ChatRequestAgentPart ? part.agent.id : + ''; result += `[${part.text}](${variableRefUrl}?${title})`; } @@ -110,74 +108,3 @@ export class ChatMarkdownDecorationsRenderer { } } } - -export interface IMarkdownVulnerability { - title: string; - description: string; - range: IRange; -} - -export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } { - const vulnerabilities: IMarkdownVulnerability[] = []; - let newText = text; - let match: RegExpExecArray | null; - while ((match = /(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) { - const [full, details, content] = match; - const start = match.index; - const textBefore = newText.substring(0, start); - const linesBefore = textBefore.split('\n').length - 1; - const linesInside = content.split('\n').length - 1; - - const previousNewlineIdx = textBefore.lastIndexOf('\n'); - const startColumn = start - (previousNewlineIdx + 1) + 1; - const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n'); - const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1; - - try { - const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details)); - vulnDetails.forEach(({ title, description }) => - vulnerabilities.push({ - title, description, range: - { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn } - })); - } catch (err) { - // Something went wrong with encoding this text, just ignore it - } - newText = newText.substring(0, start) + content + newText.substring(start + full.length); - } - - return { newText, vulnerabilities }; -} - -const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI - -export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { - const result: Exclude[] = []; - for (const item of response) { - const previousItem = result[result.length - 1]; - if (item.kind === 'inlineReference') { - const location = 'uri' in item.inlineReference ? item.inlineReference : { uri: item.inlineReference }; - const printUri = URI.parse(contentRefUrl).with({ fragment: JSON.stringify(location) }); - const markdownText = `[${item.name || basename(location.uri)}](${printUri.toString()})`; - if (previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; - } else { - result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); - } - } else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; - } else if (item.kind === 'markdownVuln') { - const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); - const markdownText = `${item.content.value}`; - if (previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; - } else { - result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); - } - } else { - result.push(item); - } - } - - return result; -} diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index 6e06936d15d..7890d192991 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -10,6 +10,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Selection } from 'vs/editor/common/core/selection'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -19,6 +20,7 @@ import { editorBackground, inputBackground, quickInputBackground, quickInputFore import { IChatWidgetService, IQuickChatService, IQuickChatOpenOptions } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -75,10 +77,12 @@ export class QuickChatService extends Disposable implements IQuickChatService { open(providerId?: string, options?: IQuickChatOpenOptions): void { if (this._input) { if (this._currentChat && options?.query) { + this._currentChat.focus(); this._currentChat.setValue(options.query, options.selection); if (!options.isPartialQuery) { this._currentChat.acceptInput(); } + return; } return this.focus(); } @@ -225,8 +229,9 @@ class QuickChat extends Disposable { this.widget = this._register( scopedInstantiationService.createInstance( ChatWidget, + ChatAgentLocation.Panel, { resource: true }, - { renderInputOnTop: true, renderStyle: 'compact' }, + { renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide } }, { listForeground: quickInputForeground, listBackground: quickInputBackground, diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts deleted file mode 100644 index 39aa0aafad3..00000000000 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts +++ /dev/null @@ -1,96 +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 'vs/css!./chatSlashCommandContentWidget'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { Range } from 'vs/editor/common/core/range'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget } from 'vs/editor/browser/editorBrowser'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { localize } from 'vs/nls'; -import * as aria from 'vs/base/browser/ui/aria/aria'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; - -export class SlashCommandContentWidget extends Disposable implements IContentWidget { - private _domNode = document.createElement('div'); - private _lastSlashCommandText: string | undefined; - private _isVisible = false; - - constructor(private _editor: ICodeEditor) { - super(); - - this._domNode.toggleAttribute('hidden', true); - this._domNode.classList.add('chat-slash-command-content-widget'); - - // If backspace at a slash command boundary, remove the slash command - this._register(this._editor.onKeyDown((e) => this._handleKeyDown(e))); - } - - override dispose() { - this.hide(); - super.dispose(); - } - - show() { - if (!this._isVisible) { - this._isVisible = true; - this._domNode.toggleAttribute('hidden', false); - this._editor.addContentWidget(this); - } - } - - hide() { - if (this._isVisible) { - this._isVisible = false; - this._domNode.toggleAttribute('hidden', true); - this._editor.removeContentWidget(this); - } - } - - setCommandText(slashCommand: string) { - this._domNode.innerText = `/${slashCommand} `; - this._lastSlashCommandText = slashCommand; - } - - getId() { - return 'chat-slash-command-content-widget'; - } - - getDomNode() { - return this._domNode; - } - - getPosition() { - return { position: { lineNumber: 1, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] }; - } - - beforeRender(): null { - const lineHeight = this._editor.getOption(EditorOption.lineHeight); - this._domNode.style.lineHeight = `${lineHeight - 2 /*padding*/}px`; - return null; - } - - private _handleKeyDown(e: IKeyboardEvent) { - if (e.keyCode !== KeyCode.Backspace) { - return; - } - - const firstLine = this._editor.getModel()?.getLineContent(1); - const selection = this._editor.getSelection(); - const withSlash = `/${this._lastSlashCommandText} `; - if (!firstLine?.startsWith(withSlash) || !selection?.isEmpty() || selection?.startLineNumber !== 1 || selection?.startColumn !== withSlash.length + 1) { - return; - } - - // Allow to undo the backspace - this._editor.executeEdits('chat-slash-command', [{ - range: new Range(1, 1, 1, selection.startColumn), - text: null - }]); - - // Announce the deletion - aria.alert(localize('exited slash command mode', 'Exited {0} mode', this._lastSlashCommandText)); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 147b267245c..9547c45ba36 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -8,12 +8,12 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IOffsetRange } from 'vs/editor/common/core/offsetRange'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatDynamicVariableModel } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IParsedChatRequest, ChatRequestVariablePart, ChatRequestDynamicVariablePart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatVariablesService, IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IDynamicVariable, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestDynamicVariablePart, ChatRequestVariablePart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; interface IChatData { data: IChatVariableData; @@ -31,7 +31,7 @@ export class ChatVariablesService implements IChatVariablesService { } async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { - let resolvedVariables: { name: string; range: IOffsetRange; values: IChatRequestVariableValue[] }[] = []; + let resolvedVariables: IChatRequestVariableEntry[] = []; const jobs: Promise[] = []; prompt.parts @@ -39,10 +39,16 @@ export class ChatVariablesService implements IChatVariablesService { if (part instanceof ChatRequestVariablePart) { 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 }; + const references: IChatContentReference[] = []; + const variableProgressCallback = (item: IChatVariableResolverProgress) => { + if (item.kind === 'reference') { + references.push(item); + return; } + progress(item); + }; + jobs.push(data.resolver(prompt.text, part.variableArg, model, variableProgressCallback, token).then(values => { + resolvedVariables[i] = { name: part.variableName, range: part.range, values: values ?? [], references }; }).catch(onUnexpectedExternalError)); } } else if (part instanceof ChatRequestDynamicVariablePart) { @@ -55,17 +61,30 @@ export class ChatVariablesService implements IChatVariablesService { resolvedVariables = coalesce(resolvedVariables); // "reverse", high index first so that replacement is simple - resolvedVariables.sort((a, b) => b.range.start - a.range.start); + resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); return { variables: resolvedVariables, }; } + 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()); } + getVariable(name: string): IChatVariableData | undefined { + return this._resolver.get(name.toLowerCase())?.data; + } + getVariables(): Iterable> { const all = Iterable.map(this._resolver.values(), data => data.data); return Iterable.filter(all, data => !data.hidden); diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index fabf8073013..1a0a353ac1d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -23,6 +23,7 @@ import { SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IChatViewPane } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -133,6 +134,7 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { this._widget = this._register(scopedInstantiationService.createInstance( ChatWidget, + ChatAgentLocation.Panel, { viewId: this.id }, { supportsFileReferences: true }, { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index ab61b6bde4f..47198878365 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -5,37 +5,43 @@ 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 { Emitter, Event } 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 { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, 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 { ChatRequestAgentPart, 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 { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; 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.$; @@ -91,13 +97,20 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidAcceptInput = this._register(new Emitter()); readonly onDidAcceptInput = this._onDidAcceptInput.event; + private _onDidChangeParsedInput = this._register(new Emitter()); + readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; + private _onDidChangeHeight = this._register(new Emitter()); readonly onDidChangeHeight = this._onDidChangeHeight.event; + private readonly _onDidChangeContentHeight = new Emitter(); + readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; + private contribs: IChatWidgetContrib[] = []; private tree!: WorkbenchObjectTree; private renderer!: ChatListItemRenderer; + private readonly _codeBlockModelCollection: CodeBlockModelCollection; private inputPart!: ChatInputPart; private editorOptions!: ChatEditorOptions; @@ -108,6 +121,8 @@ export class ChatWidget extends Disposable implements IChatWidget { private bodyDimension: dom.Dimension | undefined; private visibleChangeCount = 0; private requestInProgress: IContextKey; + private agentInInput: IContextKey; + private _visible = false; public get visible() { return this._visible; @@ -139,32 +154,87 @@ export class ChatWidget extends Disposable implements IChatWidget { private parsedChatRequest: IParsedChatRequest | undefined; get parsedInput() { if (this.parsedChatRequest === undefined) { - this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput()); + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent }); + + this.agentInInput.set((!!this.parsedChatRequest.parts.find(part => part instanceof ChatRequestAgentPart))); } return this.parsedChatRequest; } constructor( + readonly location: ChatAgentLocation, 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, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatWidgetService chatWidgetService: IChatWidgetService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILogService private readonly _logService: ILogService, - @IThemeService private readonly _themeService: IThemeService + @IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService, + @ILogService private readonly logService: ILogService, + @IThemeService private readonly themeService: IThemeService, + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, ) { super(); CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); + CONTEXT_CHAT_LOCATION.bindTo(contextKeyService).set(location); + this.agentInInput = CONTEXT_CHAT_INPUT_HAS_AGENT.bindTo(contextKeyService); 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; + })); + } + + private _lastSelectedAgent: IChatAgentData | undefined; + set lastSelectedAgent(agent: IChatAgentData | undefined) { + this.parsedChatRequest = undefined; + this._lastSelectedAgent = agent; + this._onDidChangeParsedInput.fire(); + } + + get lastSelectedAgent(): IChatAgentData | undefined { + return this._lastSelectedAgent; } get supportsFileReferences(): boolean { @@ -175,6 +245,10 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.viewModel?.providerId || ''; } + get input(): ChatInputPart { + return this.inputPart; + } + get inputEditor(): ICodeEditor { return this.inputPart.inputEditor; } @@ -183,6 +257,10 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.inputPart.inputUri; } + get contentHeight(): number { + return dom.getTotalHeight(this.inputPart.element) + this.tree.contentHeight; + } + render(parent: HTMLElement): void { const viewId = 'viewId' in this.viewContext ? this.viewContext.viewId : undefined; this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground)); @@ -195,10 +273,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this.listContainer = dom.append(this.container, $(`.interactive-list`)); } else { this.listContainer = dom.append(this.container, $(`.interactive-list`)); - this.createInput(this.container); + this.createInput(this.container, { renderFollowups: true, renderStyle }); } - this.createList(this.listContainer, { renderStyle }); + this.createList(this.listContainer, { renderStyle, editableCodeBlock: this.viewOptions.editableCodeBlocks }); this._register(this.editorOptions.onDidChange(() => this.onDidStyleChange())); this.onDidStyleChange(); @@ -213,7 +291,7 @@ export class ChatWidget extends Disposable implements IChatWidget { try { return this._register(this.instantiationService.createInstance(contrib, this)); } catch (err) { - this._logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err)); + this.logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err)); return undefined; } }).filter(isDefined); @@ -232,6 +310,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; @@ -324,7 +405,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); - const delegate = scopedInstantiationService.createInstance(ChatListDelegate); + const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200); const rendererDelegate: IChatRendererDelegate = { getListLength: () => this.tree.getNode(null).visibleChildrenCount, onDidScroll: this.onDidScroll, @@ -338,8 +419,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderer = this._register(scopedInstantiationService.createInstance( ChatListItemRenderer, this.editorOptions, + this.location, options, rendererDelegate, + this._codeBlockModelCollection, overflowWidgetsContainer, )); this._register(this.renderer.onDidClickFollowup(item => { @@ -358,9 +441,10 @@ export class ChatWidget extends Disposable implements IChatWidget { horizontalScrolling: false, supportDynamicHeights: true, hideTwistiesOfChildlessElements: true, - accessibilityProvider: this._instantiationService.createInstance(ChatAccessibilityProvider), + accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider), keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO setRowLineHeight: false, + filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined, overrideStyles: { listFocusBackground: this.styles.listBackground, listInactiveFocusBackground: this.styles.listBackground, @@ -377,7 +461,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(); @@ -424,13 +508,19 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.previousTreeScrollHeight = this.tree.scrollHeight; + this._onDidChangeContentHeight.fire(); } private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' }): void { - this.inputPart = this._register(this.instantiationService.createInstance(ChatInputPart, { - renderFollowups: options?.renderFollowups ?? true, - renderStyle: options?.renderStyle, - })); + this.inputPart = this._register(this.instantiationService.createInstance(ChatInputPart, + this.location, + { + renderFollowups: options?.renderFollowups ?? true, + renderStyle: options?.renderStyle, + menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, + editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, + } + )); this.inputPart.render(container, '', this); this._register(this.inputPart.onDidLoadInputState(state => { @@ -447,12 +537,21 @@ export class ChatWidget extends Disposable implements IChatWidget { } let msg = ''; - if (e.followup.agentId !== this.chatAgentService.getDefaultAgent()?.id) { - msg = `${chatAgentLeader}${e.followup.agentId} `; + if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { + const agent = this.chatAgentService.getAgent(e.followup.agentId); + if (!agent) { + return; + } + + this.lastSelectedAgent = agent; + msg = `${chatAgentLeader}${agent.name} `; if (e.followup.subCommand) { msg += `${chatSubcommandLeader}${e.followup.subCommand} `; } + } else if (!e.followup.agentId && e.followup.subCommand && this.chatSlashCommandService.hasCommand(e.followup.subCommand)) { + msg = `${chatSubcommandLeader}${e.followup.subCommand} `; } + msg += e.followup.message; this.acceptInput(msg); @@ -474,15 +573,41 @@ 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.inputPart.onDidChangeHeight(() => { + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + this._onDidChangeContentHeight.fire(); + })); + this._register(this.inputEditor.onDidChangeModelContent(() => this.updateImplicitContextKinds())); + this._register(this.chatAgentService.onDidChangeAgents(() => { + if (this.viewModel) { + this.updateImplicitContextKinds(); + } + })); } private onDidStyleChange(): void { this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? ''); this.container.style.setProperty('--vscode-interactive-session-foreground', this.editorOptions.configuration.foreground?.toString() ?? ''); - this.container.style.setProperty('--vscode-chat-list-background', this._themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); + this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); + } + + private updateImplicitContextKinds() { + if (!this.viewModel) { + return; + } + this.parsedChatRequest = undefined; + const agentAndSubcommand = extractAgentAndCommand(this.parsedInput); + const currentAgent = agentAndSubcommand.agentPart?.agent ?? this.chatAgentService.getDefaultAgent(this.location); + 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 { @@ -490,12 +615,19 @@ export class ChatWidget extends Disposable implements IChatWidget { 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.viewModelDisposables.add(this.viewModel.onDidChange(e => { - this.requestInProgress.set(this.viewModel!.requestInProgress); + this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection); + this.viewModelDisposables.add(Event.accumulate(this.viewModel.onDidChange, 0)(events => { + if (!this.viewModel) { + return; + } + + this.requestInProgress.set(this.viewModel.requestInProgress); + this.onDidChangeItems(); - if (e?.kind === 'addRequest') { + if (events.some(e => e?.kind === 'addRequest')) { revealLastElement(this.tree); this.focusInput(); } @@ -519,6 +651,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(); revealLastElement(this.tree); } + + this.updateImplicitContextKinds(); } getFocus(): ChatTreeItem | undefined { @@ -540,6 +674,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this.tree.domFocus(); } + refilter() { + this.tree.refilter(); + } + setInputPlaceholder(placeholder: string): void { this.viewModel?.setInputPlaceholder(placeholder); } @@ -579,12 +717,12 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onDidAcceptInput.fire(); const editorValue = this.getInput(); - const requestId = this._chatAccessibilityService.acceptRequest(); + const requestId = this.chatAccessibilityService.acceptRequest(); const input = !opts ? editorValue : '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, this.location, { selectedAgent: this._lastSelectedAgent }); if (result) { const inputState = this.collectInputState(); @@ -593,7 +731,7 @@ export class ChatWidget extends Disposable implements IChatWidget { result.responseCompletePromise.then(async () => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; - this._chatAccessibilityService.acceptResponse(lastResponse, requestId); + this.chatAccessibilityService.acceptResponse(lastResponse, requestId); }); } } @@ -634,7 +772,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 +819,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 +863,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 +898,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/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts b/src/vs/workbench/contrib/chat/browser/codeBlockContextProviderService.ts new file mode 100644 index 00000000000..8c790c54040 --- /dev/null +++ b/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/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 17b169bc4f1..01985bddc9b 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -8,19 +8,16 @@ import 'vs/css!./codeBlockPart'; 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 { Emitter } from 'vs/base/common/event'; +import { Disposable } 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/codeEditorWidget'; +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'; @@ -41,36 +38,29 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; -import { IMarkdownVulnerability } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { IMarkdownVulnerability } from '../common/annotations'; +import { TabFocus } from 'vs/editor/browser/config/tabFocus'; 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 +102,13 @@ export function parseLocalFileData(text: string) { export interface ICodeBlockActionContext { code: string; - languageId: string; + languageId?: string; codeBlockIndex: number; element: unknown; } - -export interface ICodeBlockPart { - readonly onDidChangeContentHeight: Event; - readonly element: HTMLElement; - readonly uri: URI; - layout(width: number): void; - render(data: Data, width: number): Promise; - focus(): void; - reset(): unknown; - dispose(): void; -} - const defaultCodeblockPadding = 10; -abstract class BaseCodeBlockPart extends Disposable implements ICodeBlockPart { +export class CodeBlockPart extends Disposable { protected readonly _onDidChangeContentHeight = this._register(new Emitter()); public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; @@ -138,9 +116,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 +133,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'); @@ -171,8 +152,16 @@ abstract class BaseCodeBlockPart extends Disposable padding: { top: defaultCodeblockPadding, bottom: defaultCodeblockPadding }, mouseWheelZoom: false, scrollbar: { + vertical: 'hidden', alwaysConsumeMouseWheel: false }, + definitionLinkOpensInPeek: false, + gotoLocation: { + multiple: 'goto', + multipleDeclarations: 'goto', + multipleDefinitions: 'goto', + multipleImplementations: 'goto', + }, ariaLabel: localize('chat.codeBlockHelp', 'Code block'), overflowWidgetsDomNode, ...this.getEditorOptionsFromConfig(), @@ -187,6 +176,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 +243,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 +311,23 @@ 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 | undefined) { + this.currentCodeBlockData = data; 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 +335,17 @@ 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) }); + if (editable) { + this._register(this.editor.onDidFocusEditorWidget(() => TabFocus.setTabFocusMode(true))); + this._register(this.editor.onDidBlurEditorWidget(() => TabFocus.setTabFocusMode(false))); + } + 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 +358,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).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 +393,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/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 7b6bb6c1c2b..43864b83521 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/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/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 791f9e41fd3..45bdb56ceca 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -3,8 +3,8 @@ * 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 { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Position } from 'vs/editor/common/core/position'; @@ -15,7 +15,8 @@ import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -25,9 +26,8 @@ import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/brows import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; 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 { ChatAgentLocation, 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'; @@ -39,8 +39,8 @@ const placeholderDecorationType = 'chat-session-detail'; const slashCommandTextDecorationType = 'chat-session-text'; const variableTextDecorationType = 'chat-variable-text'; -function agentAndCommandToKey(agent: string, subcommand: string | undefined): string { - return subcommand ? `${agent}__${subcommand}` : agent; +function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string { + return subcommand ? `${agent.id}__${subcommand}` : agent.id; } class InputEditorDecorations extends Disposable { @@ -66,13 +66,14 @@ class InputEditorDecorations extends Disposable { this.updateInputEditorDecorations(); this._register(this.widget.inputEditor.onDidChangeModelContent(() => this.updateInputEditorDecorations())); + this._register(this.widget.onDidChangeParsedInput(() => this.updateInputEditorDecorations())); this._register(this.widget.onDidChangeViewModel(() => { this.registerViewModelListeners(); this.previouslyUsedAgents.clear(); this.updateInputEditorDecorations(); })); this._register(this.widget.onDidSubmitAgent((e) => { - this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent.id, e.slashCommand?.name)); + this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent, e.slashCommand?.name)); })); this._register(this.chatAgentService.onDidChangeAgents(() => this.updateInputEditorDecorations())); @@ -126,8 +127,7 @@ class InputEditorDecorations extends Disposable { } if (!inputValue) { - const viewModelPlaceholder = this.widget.viewModel?.inputPlaceholder; - const placeholder = viewModelPlaceholder ?? ''; + const defaultAgent = this.chatAgentService.getDefaultAgent(this.widget.location); const decoration: IDecorationOptions[] = [ { range: { @@ -138,7 +138,7 @@ class InputEditorDecorations extends Disposable { }, renderOptions: { after: { - contentText: placeholder, + contentText: viewModel.inputPlaceholder ?? defaultAgent?.description ?? '', color: this.getPlaceholderColor() } } @@ -175,14 +175,14 @@ class InputEditorDecorations extends Disposable { const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart); if (onlyAgentAndWhitespace) { // Agent reference with no other text - show the placeholder - const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, undefined)); + const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, undefined)); const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentPart.agent.metadata.followupPlaceholder; - if (agentPart.agent.metadata.description && exactlyOneSpaceAfterPart(agentPart)) { + if (agentPart.agent.description && exactlyOneSpaceAfterPart(agentPart)) { placeholderDecoration = [{ range: getRangeForPlaceholder(agentPart), renderOptions: { after: { - contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.metadata.description, + contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.description, color: this.getPlaceholderColor(), } } @@ -193,7 +193,7 @@ class InputEditorDecorations extends Disposable { const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); if (onlyAgentCommandAndWhitespace) { // Agent reference and subcommand with no other text - show the placeholder - const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, agentSubcommandPart.command.name)); + const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name)); const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder; if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) { placeholderDecoration = [{ @@ -212,9 +212,12 @@ class InputEditorDecorations extends Disposable { const textDecorations: IDecorationOptions[] | undefined = []; if (agentPart) { - textDecorations.push({ range: agentPart.editorRange }); + const isDupe = !!this.chatAgentService.getAgents().find(other => other.name === agentPart.agent.name && other.id !== agentPart.agent.id); + const id = isDupe ? `(${agentPart.agent.id}) ` : ''; + const agentHover = `${id}${agentPart.agent.description}`; + textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) }); if (agentSubcommandPart) { - textDecorations.push({ range: agentSubcommandPart.editorRange }); + textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); } } @@ -249,9 +252,9 @@ class InputEditorSlashCommandMode extends Disposable { private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) { let value: string | undefined; if (slashCommand && slashCommand.isSticky) { - value = `${chatAgentLeader}${agent.id} ${chatSubcommandLeader}${slashCommand.name} `; + value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `; } else if (agent.metadata.isSticky) { - value = `${chatAgentLeader}${agent.id} `; + value = `${chatAgentLeader}${agent.name} `; } if (value) { @@ -276,7 +279,7 @@ class SlashCommandCompletions extends Disposable { triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel) { + if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { return null; } @@ -331,7 +334,7 @@ class AgentCompletions extends Disposable { triggerCharacters: ['@'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel) { + if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { return null; } @@ -348,15 +351,22 @@ class AgentCompletions extends Disposable { } const agents = this.chatAgentService.getAgents() - .filter(a => !a.metadata.isDefault); + .filter(a => !a.isDefault) + .filter(a => a.locations.includes(widget.location)); + return { - suggestions: agents.map((c, i) => { - const withAt = `@${c.id}`; + suggestions: agents.map((a, i) => { + const withAt = `@${a.name}`; + const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id); return { - label: withAt, + // Leading space is important because detail has no space at the start by design + label: isDupe ? + { label: withAt, description: a.description, detail: ` (${a.id})` } : + withAt, insertText: `${withAt} `, - detail: c.metadata.description, + detail: a.description, range: new Range(1, 1, 1, 1), + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: a, widget } satisfies AssignSelectedAgentActionArgs] }, kind: CompletionItemKind.Text, // The icons are disabled here anyway }; }) @@ -369,7 +379,7 @@ class AgentCompletions extends Disposable { triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel) { + if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { return; } @@ -399,10 +409,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, @@ -423,7 +431,7 @@ class AgentCompletions extends Disposable { provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); const viewModel = widget?.viewModel; - if (!widget || !viewModel) { + if (!widget || !viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { return; } @@ -432,42 +440,45 @@ class AgentCompletions extends Disposable { return null; } - const agents = this.chatAgentService.getAgents(); - const all = agents.map(agent => agent.provideSlashCommands(viewModel.model, getHistoryEntriesFromModel(viewModel.model), token)); - const commands = await raceCancellation(Promise.all(all), token); - - if (!commands) { - return; - } + const agents = this.chatAgentService.getAgents() + .filter(a => a.locations.includes(widget.location)); const justAgents: CompletionItem[] = agents - .filter(a => !a.metadata.isDefault) + .filter(a => !a.isDefault) .map(agent => { - const agentLabel = `${chatAgentLeader}${agent.id}`; + const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id); + const detail = agent.description; + const agentLabel = `${chatAgentLeader}${agent.name}`; + return { - label: { label: agentLabel, description: agent.metadata.description }, - filterText: `${chatSubcommandLeader}${agent.id}`, + label: isDupe ? + { label: agentLabel, description: agent.description, detail: ` (${agent.id})` } : + agentLabel, + detail, + filterText: `${chatSubcommandLeader}${agent.name}`, insertText: `${agentLabel} `, range: new Range(1, 1, 1, 1), kind: CompletionItemKind.Text, sortText: `${chatSubcommandLeader}${agent.id}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, }; }); return { suggestions: justAgents.concat( - agents.flatMap((agent, i) => commands[i].map((c, i) => { - const agentLabel = `${chatAgentLeader}${agent.id}`; + agents.flatMap(agent => agent.slashCommands.map((c, i) => { + const agentLabel = `${chatAgentLeader}${agent.name}`; const withSlash = `${chatSubcommandLeader}${c.name}`; return { label: { label: withSlash, description: agentLabel }, - filterText: `${chatSubcommandLeader}${agent.id}${c.name}`, + filterText: `${chatSubcommandLeader}${agent.name}${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}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, } satisfies CompletionItem; }))) }; @@ -477,6 +488,32 @@ class AgentCompletions extends Disposable { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); +interface AssignSelectedAgentActionArgs { + agent: IChatAgentData; + widget: IChatWidget; +} + +class AssignSelectedAgentAction extends Action2 { + static readonly ID = 'workbench.action.chat.assignSelectedAgent'; + + constructor() { + super({ + id: AssignSelectedAgentAction.ID, + title: '' // not displayed + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const arg: AssignSelectedAgentActionArgs = args[0]; + if (!arg || !arg.widget || !arg.agent) { + return; + } + + arg.widget.lastSelectedAgent = arg.agent; + } +} +registerAction2(AssignSelectedAgentAction); + class BuiltinDynamicCompletions extends Disposable { private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag @@ -491,7 +528,7 @@ class BuiltinDynamicCompletions extends Disposable { triggerCharacters: [chatVariableLeader], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences) { + if (!widget || !widget.supportsFileReferences || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { return null; } @@ -557,7 +594,7 @@ class VariableCompletions extends Disposable { provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget) { + if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { return null; } @@ -604,12 +641,14 @@ class ChatTokenDeleter extends Disposable { const parser = this.instantiationService.createInstance(ChatRequestParser); const inputValue = this.widget.inputEditor.getValue(); let previousInputValue: string | undefined; + let previousSelectedAgent: IChatAgentData | undefined; // 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; + previousSelectedAgent = this.widget.lastSelectedAgent; } // Don't try to handle multicursor edits right now @@ -617,7 +656,7 @@ class ChatTokenDeleter extends Disposable { // If this was a simple delete, try to find out whether it was inside a token if (!change.text && this.widget.viewModel) { - const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue); + const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, ChatAgentLocation.Panel, { selectedAgent: previousSelectedAgent }); // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart); @@ -637,7 +676,8 @@ class ChatTokenDeleter extends Disposable { } previousInputValue = this.widget.inputEditor.getValue(); - }); + previousSelectedAgent = this.widget.lastSelectedAgent; + })); } } ChatWidget.CONTRIBS.push(ChatTokenDeleter); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 201d867d1e4..e29377825f0 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -166,6 +166,11 @@ background-color: var(--vscode-chat-list-background); } +.interactive-item-container.interactive-request .header .monaco-toolbar { + /* Take the partially-transparent background color override for request rows */ + background-color: inherit; +} + .interactive-item-container .header .monaco-toolbar .checked.action-label, .interactive-item-container .header .monaco-toolbar .checked.action-label:hover { color: var(--vscode-inputOption-activeForeground) !important; @@ -337,17 +342,20 @@ 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; } +.interactive-session .interactive-input-part.compact .interactive-input-and-execute-toolbar { + margin-bottom: 0; +} + .interactive-session .interactive-input-and-side-toolbar { display: flex; gap: 4px; @@ -358,6 +366,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; @@ -369,6 +381,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 { @@ -412,11 +432,24 @@ } .interactive-session .interactive-input-part { + margin: 0px 20px; padding: 12px 0px; display: flex; flex-direction: column; } +.interactive-session .interactive-input-part.compact { + margin: 0; + padding: 6px 0px; +} + +.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; @@ -438,10 +471,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; } @@ -485,10 +514,11 @@ .quick-input-widget .interactive-session .interactive-input-part { padding: 8px 6px 6px 6px; + margin: 0 3px; } .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/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts new file mode 100644 index 00000000000..f55eb1ffd9a --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { basename } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IRange } from 'vs/editor/common/core/range'; +import { IChatProgressRenderableResponseContent, IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatAgentMarkdownContentWithVulnerability, IChatAgentVulnerabilityDetails, IChatContentInlineReference, IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; + +export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI + +export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { + const result: Exclude[] = []; + for (const item of response) { + const previousItem = result[result.length - 1]; + if (item.kind === 'inlineReference') { + const location = 'uri' in item.inlineReference ? item.inlineReference : { uri: item.inlineReference }; + const printUri = URI.parse(contentRefUrl).with({ fragment: JSON.stringify(location) }); + const markdownText = `[${item.name || basename(location.uri)}](${printUri.toString()})`; + if (previousItem?.kind === 'markdownContent') { + result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + } else { + result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); + } + } else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent') { + result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + } else if (item.kind === 'markdownVuln') { + const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); + const markdownText = `${item.content.value}`; + if (previousItem?.kind === 'markdownContent') { + result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + } else { + result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); + } + } else { + result.push(item); + } + } + + return result; +} + +export interface IMarkdownVulnerability { + readonly title: string; + readonly description: string; + readonly range: IRange; +} + +export function annotateVulnerabilitiesInText(response: ReadonlyArray): readonly IChatMarkdownContent[] { + const result: IChatMarkdownContent[] = []; + for (const item of response) { + const previousItem = result[result.length - 1]; + if (item.kind === 'markdownContent') { + if (previousItem?.kind === 'markdownContent') { + result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + } else { + result.push(item); + } + } else if (item.kind === 'markdownVuln') { + const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); + const markdownText = `${item.content.value}`; + if (previousItem?.kind === 'markdownContent') { + result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + } else { + result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); + } + } + } + + return result; +} + +export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } { + const vulnerabilities: IMarkdownVulnerability[] = []; + let newText = text; + let match: RegExpExecArray | null; + while ((match = /(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) { + const [full, details, content] = match; + const start = match.index; + const textBefore = newText.substring(0, start); + const linesBefore = textBefore.split('\n').length - 1; + const linesInside = content.split('\n').length - 1; + + const previousNewlineIdx = textBefore.lastIndexOf('\n'); + const startColumn = start - (previousNewlineIdx + 1) + 1; + const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n'); + const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1; + + try { + const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details)); + vulnDetails.forEach(({ title, description }) => vulnerabilities.push({ + title, description, range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn } + })); + } catch (err) { + // Something went wrong with encoding this text, just ignore it + } + newText = newText.substring(0, start) + content + newText.substring(start + full.length); + } + + return { newText, vulnerabilities }; +} + diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index b1ef64dae9d..2aa2893de49 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -7,13 +7,15 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { 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 { 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 +26,60 @@ export interface IChatAgentHistoryEntry { result: IChatAgentResult; } +export enum ChatAgentLocation { + Panel = 'panel', + Terminal = 'terminal', + Notebook = 'notebook', + Editor = 'editor' +} + +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; + name: string; + description?: 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 { @@ -105,102 +118,205 @@ export interface IChatAgentResult { export const IChatAgentService = createDecorator('chatAgentService'); +interface IChatAgentEntry { + data: IChatAgentData; + impl?: IChatAgentImplementation; +} + export interface IChatAgentService { _serviceBrand: undefined; /** - * undefined when an agent was removed + * undefined when an agent was removed IChatAgent */ readonly onDidChangeAgents: Event; - registerAgent(agent: IChatAgent): IDisposable; - 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; - getDefaultAgent(): IChatAgent | undefined; - getSecondaryAgent(): IChatAgent | undefined; - hasAgent(id: string): boolean; + registerAgent(id: string, data: IChatAgentData): IDisposable; + registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; + invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + getAgent(id: string): IChatAgentData | undefined; + getAgents(): IChatAgentData[]; + getActivatedAgents(): Array; + getAgentsByName(name: string): IChatAgentData[]; + getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined; + getSecondaryAgent(): IChatAgentData | undefined; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } -export class ChatAgentService extends Disposable implements IChatAgentService { +export class ChatAgentService implements IChatAgentService { public static readonly AGENT_LEADER = '@'; declare _serviceBrand: undefined; - private readonly _agents = new Map(); + private _agents: IChatAgentEntry[] = []; - private readonly _onDidChangeAgents = this._register(new Emitter()); + private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; - override dispose(): void { - super.dispose(); - this._agents.clear(); + constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService + ) { } + + registerAgent(id: string, data: IChatAgentData): IDisposable { + const existingAgent = this.getAgent(id); + if (existingAgent) { + throw new Error(`Agent already registered: ${JSON.stringify(id)}`); + } + + const that = this; + const commands = data.slashCommands; + data = { + ...data, + get slashCommands() { + return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); + } + }; + const entry = { data }; + this._agents.push(entry); + return toDisposable(() => { + this._agents = this._agents.filter(a => a !== entry); + this._onDidChangeAgents.fire(undefined); + }); } - registerAgent(agent: IChatAgent): IDisposable { - if (this._agents.has(agent.id)) { - throw new Error(`Already registered an agent with id ${agent.id}`); + registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable { + const entry = this._getAgentEntry(id); + if (!entry) { + throw new Error(`Unknown agent: ${JSON.stringify(id)}`); } - this._agents.set(agent.id, { agent }); - this._onDidChangeAgents.fire(agent); + + if (entry.impl) { + throw new Error(`Agent already has implementation: ${JSON.stringify(id)}`); + } + + entry.impl = agentImpl; + this._onDidChangeAgents.fire(new MergedChatAgent(entry.data, agentImpl)); return toDisposable(() => { - if (this._agents.delete(agent.id)) { - this._onDidChangeAgents.fire(undefined); - } + entry.impl = undefined; + this._onDidChangeAgents.fire(undefined); + }); + } + + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { + const agent = { data, impl: agentImpl }; + this._agents.push(agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); + + return toDisposable(() => { + this._agents = this._agents.filter(a => a !== agent); + 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._getAgentEntry(id); + if (!agent?.impl) { + throw new Error(`No activated agent with id ${JSON.stringify(id)} registered`); } - data.agent.metadata = { ...data.agent.metadata, ...updateMetadata }; - this._onDidChangeAgents.fire(data.agent); + agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; + this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); + } + + getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined { + return this.getActivatedAgents().find(a => !!a.isDefault && a.locations.includes(location)); } - getDefaultAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isDefault)?.agent; + 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; + private _getAgentEntry(id: string): IChatAgentEntry | undefined { + return this._agents.find(a => a.data.id === id); } - getAgents(): Array { - return Array.from(this._agents.values(), v => v.agent); + getAgent(id: string): IChatAgentData | undefined { + return this._getAgentEntry(id)?.data; + } + + /** + * Returns all agent datas that exist- static registered and dynamic ones. + */ + getAgents(): IChatAgentData[] { + return this._agents.map(entry => entry.data); } - 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; + getAgentsByName(name: string): IChatAgentData[] { + return this.getAgents().filter(a => a.name === name); } 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}`); + const data = this._getAgentEntry(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 { - const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + const data = this._getAgentEntry(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 name(): string { return this.data.name ?? ''; } + get description(): string { return this.data.description ?? ''; } + 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/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index d5cb83c170a..417b72ff410 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -5,8 +5,10 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; export const CONTEXT_RESPONSE_VOTE = new RawContextKey('chatSessionResponseVote', '', { 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.") }); +export const CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND = new RawContextKey('chatSessionResponseDetectedAgentOrCommand', false, { type: 'boolean', description: localize('chatSessionResponseDetectedAgentOrCommand', "When the agent or command was automatically detected") }); export const CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING = new RawContextKey('chatResponseSupportsIssueReporting', false, { type: 'boolean', description: localize('chatResponseSupportsIssueReporting', "True when the current chat response supports issue reporting.") }); export const CONTEXT_RESPONSE_FILTERED = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); @@ -15,8 +17,11 @@ export const CONTEXT_RESPONSE = new RawContextKey('chatResponse', false export const CONTEXT_REQUEST = new RawContextKey('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") }); export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") }); +export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); export const CONTEXT_PROVIDER_EXISTS = new RawContextKey('hasChatProvider', false, { type: 'boolean', description: localize('hasChatProvider', "True when some chat provider has been registered.") }); export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey('chatCursorAtTop', false); +export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey('chatInputHasAgent', false); +export const CONTEXT_CHAT_LOCATION = new RawContextKey('chatLocation', undefined); diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatContributionService.ts index 2c43c2af703..7d7452b1957 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IChatProviderContribution { @@ -26,3 +27,30 @@ 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 { + id: string; + name: string; + description?: string; + isDefault?: boolean; + isSticky?: boolean; + commands?: IRawChatCommandContribution[]; + defaultImplicitVariables?: string[]; + locations?: RawChatParticipantLocation[]; +} + +export interface IChatParticipantContribution extends IRawChatParticipantContribution { + // Participant id is extensionId + name + extensionId: ExtensionIdentifier; +} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index c8e47cb8b16..84913ffc2c2 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -8,21 +8,36 @@ import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; 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 { TextEdit } from 'vs/editor/common/languages'; +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[]; + references?: IChatContentReference[]; +} + +export interface IChatRequestVariableData { + variables: IChatRequestVariableEntry[]; +} + export interface IChatRequestModel { readonly id: string; readonly username: string; @@ -54,14 +69,16 @@ 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; readonly contentReferences: ReadonlyArray; readonly progressMessages: ReadonlyArray; readonly slashCommand?: IChatAgentCommand; + readonly agentOrSlashCommandDetected: boolean; readonly response: IResponse; + readonly edits: ResourceMap; readonly isComplete: boolean; readonly isCanceled: boolean; /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ @@ -222,6 +239,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._response; } + private _edits: ResourceMap; + public get edits(): ResourceMap { + return this._edits; + } + public get result(): IChatAgentResult | undefined { return this._result; } @@ -234,8 +256,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[]; @@ -248,6 +270,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._slashCommand; } + private _agentOrSlashCommandDetected: boolean | undefined; + public get agentOrSlashCommandDetected(): boolean { + return this._agentOrSlashCommandDetected ?? false; + } + private _usedContext: IChatUsedContext | undefined; public get usedContext(): IChatUsedContext | undefined { return this._usedContext; @@ -287,6 +314,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._followups = followups ? [...followups] : undefined; this._response = new Response(_response); + this._edits = new ResourceMap(); this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); this._id = 'response_' + ChatResponseModel.nextId++; } @@ -298,6 +326,16 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._response.updateContent(responsePart, quiet); } + updateTextEdits(uri: URI, edits: TextEdit[]) { + const array = this._edits.get(uri); + if (!array) { + this._edits.set(uri, edits); + } else { + array.push(...edits); + } + this._onDidChange.fire(); + } + /** * Apply one of the progress updates that are not part of the actual response content. */ @@ -313,6 +351,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) { this._agent = agent; this._slashCommand = slashCommand; + this._agentOrSlashCommandDetected = true; this._onDidChange.fire(); } @@ -392,7 +431,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 +444,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 +521,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 +531,34 @@ export class ChatModel extends Disposable implements IChatModel { return this._creationDate; } + private get _defaultAgent() { + return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); + } + 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 +579,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 +589,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 +601,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 { @@ -571,7 +618,7 @@ export class ChatModel extends Disposable implements IChatModel { const request = new ChatRequestModel(this, parsedRequest, variableData); if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format - revive(raw.agent) : undefined; + this.reviveSerializedAgent(raw.agent) : undefined; // Port entries from old format const result = 'responseErrorDetails' in raw ? @@ -593,6 +640,16 @@ export class ChatModel extends Disposable implements IChatModel { } } + private reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { + const agent = 'name' in raw ? + raw : + { + ...(raw as any), + name: (raw as any).id, + }; + return revive(agent); + } + private getParsedRequestFromString(message: string): IParsedChatRequest { // TODO These offsets won't be used, but chat replies need to go through the parser as well const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)]; @@ -683,10 +740,12 @@ export class ChatModel extends Disposable implements IChatModel { } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); } else if (progress.kind === 'agentDetection') { - const agent = this.chatAgentService.getAgent(progress.agentName); + const agent = this.chatAgentService.getAgent(progress.agentId); if (agent) { request.response.setAgent(agent, progress.command); } + } else if (progress.kind === 'textEdit') { + request.response.updateTextEdits(progress.uri, progress.edits); } else { this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); } @@ -748,7 +807,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 +839,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, name: r.response.agent.name, description: r.response.agent.description, 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 +877,7 @@ export interface IChatWelcomeMessageModel { readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon; } @@ -828,19 +890,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(ChatAgentLocation.Panel)?.metadata.fullName ?? ''; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | undefined { + return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.metadata.themeIcon; } } @@ -858,7 +920,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 +933,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/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 4f7654fd56a..7edda68e10e 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgent, IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -72,10 +72,10 @@ 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}`; + return `${chatAgentLeader}${this.agent.name}`; } get promptText(): string { @@ -92,6 +92,8 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart { editorRange: this.editorRange, agent: { id: this.agent.id, + name: this.agent.name, + description: this.agent.description, metadata: this.agent.metadata } }; @@ -167,10 +169,19 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed (part as ChatRequestVariablePart).variableArg ); } else if (part.kind === ChatRequestAgentPart.Kind) { + let agent = (part as ChatRequestAgentPart).agent; + if (!('name' in agent)) { + // Port old format + agent = { + ...(agent as any), + name: (agent as any).id + }; + } + return new ChatRequestAgentPart( new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, - (part as ChatRequestAgentPart).agent + agent ); } else if (part.kind === ChatRequestAgentSubcommandPart.Kind) { return new ChatRequestAgentSubcommandPart( @@ -197,3 +208,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/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index aa05f52b6d8..f60b9cb5dc3 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -6,10 +6,8 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -17,18 +15,21 @@ const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command +export interface IChatParserContext { + /** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */ + selectedAgent?: IChatAgentData; +} + export class ChatRequestParser { constructor( @IChatAgentService private readonly agentService: IChatAgentService, @IChatVariablesService private readonly variableService: IChatVariablesService, - @IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService, - @IChatService private readonly chatService: IChatService + @IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService ) { } - parseChatRequest(sessionId: string, message: string): IParsedChatRequest { + parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest { const parts: IParsedChatRequestPart[] = []; const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls - const model = this.chatService.getSession(sessionId)!; let lineNumber = 1; let column = 1; @@ -40,9 +41,9 @@ export class ChatRequestParser { if (char === chatVariableLeader) { newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts); } else if (char === chatAgentLeader) { - newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts); + newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); } else if (char === chatSubcommandLeader) { - newPart = this.tryToParseSlashCommand(model, message.slice(i), message, i, new Position(lineNumber, column), parts); + newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts); } if (!newPart) { @@ -89,18 +90,24 @@ export class ChatRequestParser { }; } - private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { - const nextVariableMatch = message.match(agentReg); - if (!nextVariableMatch) { + private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined { + const nextAgentMatch = message.match(agentReg); + if (!nextAgentMatch) { return; } - const [full, name] = nextVariableMatch; - const varRange = new OffsetRange(offset, offset + full.length); - const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + const [full, name] = nextAgentMatch; + const agentRange = new OffsetRange(offset, offset + full.length); + const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + + const agents = this.agentService.getAgentsByName(name); - const agent = this.agentService.getAgent(name); - if (!agent) { + // If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the + // context and we use that one. Otherwise just pick the first. + const agent = agents.length > 1 && context?.selectedAgent ? + context.selectedAgent : + agents[0]; + if (!agent || !agent.locations.includes(location)) { return; } @@ -121,7 +128,7 @@ export class ChatRequestParser { return; } - return new ChatRequestAgentPart(varRange, varEditorRange, agent); + return new ChatRequestAgentPart(agentRange, agentEditorRange, agent); } private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { @@ -142,7 +149,7 @@ export class ChatRequestParser { return; } - private tryToParseSlashCommand(model: IChatModel, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; @@ -171,8 +178,7 @@ export class ChatRequestParser { return; } - const subCommands = usedAgent.agent.getLastSlashCommands(model); - const subCommand = subCommands?.find(c => c.name === command); + const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); if (subCommand) { // Valid agent subcommand return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5da8d9b747b..819c41f3045 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -9,21 +9,17 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { Command, Location, ProviderResult } from 'vs/editor/common/languages'; +import { Command, Location, ProviderResult, TextEdit } from 'vs/editor/common/languages'; import { FileType } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChat { id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about - requesterUsername: string; - requesterAvatarIconUri?: URI; - responderUsername: string; - responderAvatarIconUri?: URI; - inputPlaceholder?: string; dispose?(): void; } @@ -78,8 +74,13 @@ export function isIUsedContext(obj: unknown): obj is IChatUsedContext { ); } +export interface IChatContentVariableReference { + variableName: string; + value?: URI | Location; +} + export interface IChatContentReference { - reference: URI | Location; + reference: URI | Location | IChatContentVariableReference; kind: 'reference'; } @@ -90,7 +91,7 @@ export interface IChatContentInlineReference { } export interface IChatAgentDetection { - agentName: string; + agentId: string; command?: IChatAgentCommand; kind: 'agentDetection'; } @@ -138,6 +139,12 @@ export interface IChatCommandButton { kind: 'command'; } +export interface IChatTextEdit { + uri: URI; + edits: TextEdit[]; + kind: 'textEdit'; +} + export type IChatProgress = | IChatContent | IChatMarkdownContent @@ -149,7 +156,8 @@ export type IChatProgress = | IChatContentInlineReference | IChatAgentDetection | IChatProgressMessage - | IChatCommandButton; + | IChatCommandButton + | IChatTextEdit; export interface IChatProvider { readonly id: string; @@ -288,12 +296,11 @@ export interface IChatService { /** * Returns whether the request was accepted. */ - sendRequest(sessionId: string, message: string): Promise; + sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, location?: ChatAgentLocation, parserContext?: IChatParserContext): 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/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 6a62b232974..b96d7774d76 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -5,6 +5,7 @@ import { Action } from 'vs/base/common/actions'; 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'; @@ -22,15 +23,15 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgent, 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/chatProvider'; -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 { ChatRequestParser, IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser'; +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'; @@ -197,12 +198,6 @@ export class ChatService extends Disposable implements IChatService { } this._register(storageService.onWillSaveState(() => this.saveState())); - - this._register(Event.debounce(this.chatAgentService.onDidChangeAgents, () => { }, 500)(() => { - for (const model of this._sessionModels.values()) { - this.warmSlashCommandCache(model); - } - })); } private saveState(): void { @@ -368,21 +363,15 @@ export class ChatService extends Disposable implements IChatService { this.initializeSession(model, CancellationToken.None); } - private warmSlashCommandCache(model: IChatModel, agent?: IChatAgent) { - const agents = agent ? [agent] : this.chatAgentService.getAgents(); - agents.forEach(agent => agent.provideSlashCommands(model, [], CancellationToken.None)); - } - private async initializeSession(model: ChatModel, token: CancellationToken): Promise { try { this.trace('initializeSession', `Initialize session ${model.sessionId}`); - this.warmSlashCommandCache(model); model.startInitialize(); await this.extensionService.activateByEvent(`onInteractiveSession:${model.providerId}`); 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; @@ -398,7 +387,7 @@ export class ChatService extends Disposable implements IChatService { this.trace('startSession', `Provider returned session`); - const defaultAgent = this.chatAgentService.getDefaultAgent(); + const defaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); if (!defaultAgent) { this.notificationService.notify({ severity: Severity.Error, @@ -411,12 +400,12 @@ export class ChatService extends Disposable implements IChatService { ] } }); - 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) ?? [] ); @@ -461,7 +450,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, location: ChatAgentLocation = ChatAgentLocation.Panel, parserContext?: IChatParserContext): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); if (!request.trim()) { this.trace('sendRequest', 'Rejected empty message'); @@ -484,13 +473,15 @@ export class ChatService extends Disposable implements IChatService { return; } - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request); - const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? this.chatAgentService.getDefaultAgent()!; + const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; + + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); + const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); // 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 ?? false, defaultAgent, location), agent, slashCommand: agentSlashCommandPart?.command, }; @@ -504,7 +495,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, defaultAgent: IChatAgent, location: ChatAgentLocation): 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); @@ -555,9 +546,9 @@ export class ChatService extends Disposable implements IChatService { let rawResult: IChatAgentResult | null | undefined; let agentOrCommandFollowups: Promise | undefined = undefined; - 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: [] }; @@ -566,18 +557,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 }; 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 @@ -660,11 +661,6 @@ export class ChatService extends Disposable implements IChatService { model.removeRequest(requestId); } - async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<{ responseCompletePromise: Promise } | undefined> { - this.trace('sendRequestToProvider', `sessionId: ${sessionId}`); - return await this.sendRequest(sessionId, message.message); - } - getProviders(): string[] { return Array.from(this._providers.keys()); } diff --git a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts index 3d758da3113..2d43f1c0396 100644 --- a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress } from 'vs/platform/progress/common/progress'; -import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; +import { IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 859daf171e5..dc999f8081f 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -41,6 +41,7 @@ export interface IChatVariablesService { _serviceBrand: undefined; registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable; hasVariable(name: string): boolean; + getVariable(name: string): IChatVariableData | undefined; getVariables(): Iterable>; getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? @@ -48,6 +49,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/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index a91512a6840..37290ed2015 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/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 { ResourceMap } from 'vs/base/common/map'; +import { marked } from 'vs/base/common/marked/marked'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; +import { TextEdit } from 'vs/editor/common/languages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; 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,13 +116,15 @@ 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 agentOrSlashCommandDetected: boolean; readonly response: IResponse; readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; readonly progressMessages: ReadonlyArray; + readonly edits: ResourceMap; readonly isComplete: boolean; readonly isCanceled: boolean; readonly isStale: boolean; @@ -134,6 +142,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 +153,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 +188,16 @@ export class ChatViewModel extends Disposable implements IChatViewModel { constructor( private readonly _model: IChatModel, + public readonly codeBlockModelCollection: CodeBlockModelCollection, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { 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 +206,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 +240,14 @@ 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(() => { + if (response.isComplete) { + this.updateCodeBlockTextModels(response); + } + return this._onDidChange.fire(null); + })); this._items.push(response); + this.updateCodeBlockTextModels(response); } getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel)[] { @@ -238,6 +260,60 @@ export class ChatViewModel extends Disposable implements IChatViewModel { .filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel) .forEach((item: ChatResponseViewModel) => item.dispose()); } + + updateCodeBlockTextModels(model: IChatRequestViewModel | IChatResponseViewModel) { + let content: string; + if (isRequestVM(model)) { + content = model.messageText; + } else { + content = annotateVulnerabilitiesInText(model.response.value).map(x => x.content.value).join(''); + } + + let codeBlockIndex = 0; + const renderer = new marked.Renderer(); + renderer.code = (value, languageId) => { + languageId ??= ''; + const newText = this.fixCodeText(value, languageId); + this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: newText, languageId }); + 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 +333,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.username; } - get avatarIconUri() { + get avatarIcon() { return this._model.avatarIconUri; } @@ -271,7 +347,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 +378,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() { @@ -312,6 +390,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.slashCommand; } + get agentOrSlashCommandDetected() { + return this._model.agentOrSlashCommandDetected; + } + get response(): IResponse { return this._model.response; } @@ -328,6 +410,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.progressMessages; } + get edits(): ResourceMap { + return this._model.edits; + } + get isComplete() { return this._model.isComplete; } @@ -393,7 +479,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 +530,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi export interface IChatWelcomeMessageViewModel { readonly id: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; currentRenderedHeight?: number; diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts new file mode 100644 index 00000000000..f364b9e78ab --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Range } from 'vs/editor/common/core/range'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { EndOfLinePreference } from 'vs/editor/common/model'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations'; + + +export class CodeBlockModelCollection extends Disposable { + + private readonly _models = new ResourceMap<{ + readonly model: Promise>; + vulns: readonly IMarkdownVulnerability[]; + }>(); + + constructor( + @ILanguageService private readonly languageService: ILanguageService, + @ITextModelService private readonly textModelService: ITextModelService + ) { + super(); + } + + public override dispose(): void { + super.dispose(); + this.clear(); + } + + get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } | undefined { + const uri = this.getUri(sessionId, chat, codeBlockIndex); + const entry = this._models.get(uri); + if (!entry) { + return; + } + return { model: entry.model.then(ref => ref.object), vulns: entry.vulns }; + } + + getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } { + 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, { model: ref, vulns: [] }); + return { model: ref.then(ref => ref.object), vulns: [] }; + } + + clear(): void { + this._models.forEach(async entry => (await entry.model).dispose()); + this._models.clear(); + } + + async update(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: { text: string; languageId?: string }) { + const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); + + const extractedVulns = extractVulnerabilitiesFromText(content.text); + const newText = extractedVulns.newText; + this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); + + const textModel = (await entry.model).textEditorModel; + if (content.languageId) { + const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(content.languageId); + if (vscodeLanguageId && vscodeLanguageId !== textModel.getLanguageId()) { + textModel.setLanguage(vscodeLanguageId); + } + } + + const currentText = textModel.getValue(EndOfLinePreference.LF); + if (newText === currentText) { + return; + } + + if (newText.startsWith(currentText)) { + const text = newText.slice(currentText.length); + const lastLine = textModel.getLineCount(); + const lastCol = textModel.getLineMaxColumn(lastLine); + textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]); + } else { + // console.log(`Failed to optimize setText`); + textModel.setValue(newText); + } + } + + private setVulns(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, vulnerabilities: IMarkdownVulnerability[]) { + const uri = this.getUri(sessionId, chat, codeBlockIndex); + const entry = this._models.get(uri); + if (entry) { + entry.vulns = vulnerabilities; + } + } + + 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 => { + const uriOrLocation = 'variableName' in ref.reference ? + ref.reference.value : + ref.reference; + if (!uriOrLocation) { + return; + } + + if (URI.isUri(uriOrLocation)) { + return { + uri: uriOrLocation.toJSON() + }; + } + + return { + uri: uriOrLocation.uri.toJSON(), + range: uriOrLocation.range, + }; + }) + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatProvider.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts similarity index 58% rename from src/vs/workbench/contrib/chat/common/chatProvider.ts rename to src/vs/workbench/contrib/chat/common/languageModels.ts index c393a73de98..7304d002628 100644 --- a/src/vs/workbench/contrib/chat/common/chatProvider.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -26,8 +26,9 @@ export interface IChatResponseFragment { part: string; } -export interface IChatResponseProviderMetadata { +export interface ILanguageModelChatMetadata { readonly extension: ExtensionIdentifier; + readonly identifier: string; readonly model: string; readonly description?: string; readonly auth?: { @@ -36,55 +37,55 @@ export interface IChatResponseProviderMetadata { }; } -export interface IChatResponseProvider { - metadata: IChatResponseProviderMetadata; +export interface ILanguageModelChat { + metadata: ILanguageModelChatMetadata; provideChatResponse(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; } -export const IChatProviderService = createDecorator('chatProviderService'); +export const ILanguageModelsService = createDecorator('ILanguageModelsService'); -export interface IChatProviderService { +export interface ILanguageModelsService { readonly _serviceBrand: undefined; - onDidChangeProviders: Event<{ added?: string[]; removed?: string[] }>; + onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>; - getProviders(): string[]; + getLanguageModelIds(): string[]; - lookupChatResponseProvider(identifier: string): IChatResponseProviderMetadata | undefined; + lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined; - registerChatResponseProvider(identifier: string, provider: IChatResponseProvider): IDisposable; + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; - fetchChatResponse(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; + makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; } -export class ChatProviderService implements IChatProviderService { +export class LanguageModelsService implements ILanguageModelsService { readonly _serviceBrand: undefined; - private readonly _providers: Map = new Map(); + private readonly _providers: Map = new Map(); - private readonly _onDidChangeProviders = new Emitter<{ added?: string[]; removed?: string[] }>(); - readonly onDidChangeProviders: 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(); this._providers.clear(); } - getProviders(): string[] { + getLanguageModelIds(): string[] { return Array.from(this._providers.keys()); } - lookupChatResponseProvider(identifier: string): IChatResponseProviderMetadata | undefined { + lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined { return this._providers.get(identifier)?.metadata; } - registerChatResponseProvider(identifier: string, provider: IChatResponseProvider): IDisposable { + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable { if (this._providers.has(identifier)) { 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] }); @@ -92,7 +93,7 @@ export class ChatProviderService implements IChatProviderService { }); } - fetchChatResponse(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise { + makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise { const provider = this._providers.get(identifier); if (!provider) { throw new Error(`Chat response provider with identifier ${identifier} is not registered.`); diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChat.ts index 9c92a44354a..1c93007b8cf 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/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/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css index 7a43cef7d12..f386a4a0089 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -6,23 +6,22 @@ /* * 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 .interactive-input-part .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 .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { animation: none; } /* * 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 .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: "\ead7"; + font-family: 'codicon'; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index b508d24ff6b..0e2bd19e8d5 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/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'; @@ -17,7 +17,7 @@ import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatService, KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, MENU_INLINE_CHAT_INPUT } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -42,30 +42,32 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; 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, TerminalChatContextKeys } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; +import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; 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,18 +91,29 @@ 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') { + // 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); + } + } + // Try with the chat widget service, which currently // only supports the chat view and quick chat // https://github.com/microsoft/vscode/issues/191191 @@ -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,20 +462,18 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor const holdMode = keybindingService.enableKeybindingHoldMode(id); - let acceptVoice = false; - const handle = disposableTimeout(() => { - acceptVoice = true; - 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); if (!controller) { - handle.dispose(); return; } - const session = VoiceChatSessions.getInstance(instantiationService).start(controller, context); + const session = await VoiceChatSessions.getInstance(instantiationService).start(controller, context); + let acceptVoice = false; + const handle = disposableTimeout(() => { + acceptVoice = true; + session?.setTimeoutDisabled(true); // disable accept on timeout when hold mode runs for VOICE_KEY_HOLD_THRESHOLD + }, VOICE_KEY_HOLD_THRESHOLD); await holdMode; handle.dispose(); @@ -480,7 +521,8 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { when: ContextKeyExpr.and( CanVoiceChat, FocusInChatInput.negate(), // when already in chat input, disable this action and prefer to start voice chat directly - EditorContextKeys.focus.negate() // do not steal the inline-chat keybinding + EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI } @@ -502,7 +544,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); @@ -563,24 +605,26 @@ export class StartVoiceChatAction extends Action2 { when: ContextKeyExpr.and( FocusInChatInput, // scope this action to chat input fields only EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate(), // do not steal the notebook keybinding 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 }, icon: Codicon.mic, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_GETTING_READY.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.negate()), + precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_GETTING_READY.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.negate(), TerminalChatContextKeys.requestActive.negate()), menu: [{ id: MenuId.ChatExecute, when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate()), group: 'navigation', order: -1 }, { - id: MENU_INLINE_CHAT_INPUT, - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), - group: 'main', + id: MenuId.for('terminalChatInput'), + when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate()), + group: 'navigation', order: -1 }] }); @@ -624,9 +668,9 @@ export class InstallVoiceChatAction extends Action2 { group: 'navigation', order: -1 }, { - id: MENU_INLINE_CHAT_INPUT, + id: MenuId.for('terminalChatInput'), when: HasSpeechProvider.negate(), - group: 'main', + group: 'navigation', order: -1 }] }); @@ -634,36 +678,13 @@ export class InstallVoiceChatAction extends Action2 { 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,10 +695,9 @@ 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' ) { super({ ...desc, @@ -691,7 +711,7 @@ class BaseStopListeningAction extends Action2 { menu: menu ? [{ id: menu, when: ContextKeyExpr.and(CanVoiceChat, context), - group, + group: 'navigation', order: -1 }] : undefined }); @@ -738,12 +758,12 @@ export class StopListeningInQuickChatAction extends BaseStopListeningAction { } } -export class StopListeningInInlineChatAction extends BaseStopListeningAction { +export class StopListeningInTerminalChatAction extends BaseStopListeningAction { - static readonly ID = 'workbench.action.chat.stopListeningInInlineChat'; + static readonly ID = 'workbench.action.chat.stopListeningInTerminalChat'; constructor() { - super({ id: StopListeningInInlineChatAction.ID, icon: spinningLoading }, 'inline', CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS, MENU_INLINE_CHAT_INPUT, 'main'); + super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, 'terminal', CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS, MenuId.for('terminalChatInput')); } } @@ -784,11 +804,36 @@ 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) .interactive-input-part .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 { + 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 { 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 { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; } @keyframes pulseAnimation { @@ -834,7 +879,6 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe @ISpeechService private readonly speechService: ISpeechService, @IConfigurationService private readonly configurationService: IConfigurationService, @ICommandService private readonly commandService: ICommandService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IInstantiationService instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IHostService private readonly hostService: IHostService, @@ -848,7 +892,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(); })); @@ -863,10 +907,6 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe this.handleKeywordActivation(); } })); - - this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(({ instantiationService, disposables }) => { - disposables.add(instantiationService.createInstance(KeywordActivationStatusEntry)); - })); } private updateConfiguration(): void { @@ -993,7 +1033,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(); @@ -1033,7 +1073,8 @@ class KeywordActivationStatusEntry extends Disposable { tooltip: this.speechService.hasActiveKeywordRecognition ? KeywordActivationStatusEntry.STATUS_ACTIVE : KeywordActivationStatusEntry.STATUS_INACTIVE, ariaLabel: this.speechService.hasActiveKeywordRecognition ? KeywordActivationStatusEntry.STATUS_ACTIVE : KeywordActivationStatusEntry.STATUS_INACTIVE, command: KeywordActivationStatusEntry.STATUS_COMMAND, - kind: 'prominent' + kind: 'prominent', + showInAllWindows: true }; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 08b145350e1..186a6a715c1 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, 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'; @@ -21,6 +21,6 @@ registerAction2(StopListeningAndSubmitAction); registerAction2(StopListeningInChatViewAction); registerAction2(StopListeningInChatEditorAction); registerAction2(StopListeningInQuickChatAction); -registerAction2(StopListeningInInlineChatAction); +registerAction2(StopListeningInTerminalChatAction); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.0.snap deleted file mode 100644 index c93e1967da3..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.0.snap +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - content: { - value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newlinecontent with vuln\nand\nnewlines", - isTrusted: false, - supportThemeIcons: false, - supportHtml: false - }, - kind: "markdownContent" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.0.snap deleted file mode 100644 index 50ceeb23745..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.0.snap +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - content: { - value: "some code content with vuln after", - isTrusted: false, - supportThemeIcons: false, - supportHtml: false - }, - kind: "markdownContent" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts index 35173312a8c..6d6856156ee 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -33,7 +33,7 @@ suite('ChatVariables', function () { instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatVariablesService, service); instantiationService.stub(IChatService, new MockChatService()); - instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); }); test('ChatVariables - resolveVariables', async function () { diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap similarity index 55% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.0.snap rename to src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap index c0d74bb4d15..11c9c2ef292 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap @@ -1,7 +1,7 @@ [ { content: { - value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newline", + value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newline", isTrusted: false, supportThemeIcons: false, supportHtml: false diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiline.1.snap rename to src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.1.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap new file mode 100644 index 00000000000..bc1d5cda51e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newlinecontent with vuln\nand\nnewlines", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_multiple_vulns.1.snap rename to src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.1.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap new file mode 100644 index 00000000000..229ab7c6ac4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "some code content with vuln after", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownDecorationsRenderer_extractVulnerabilitiesFromText_single_line.1.snap rename to src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.1.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index 4a241114279..913d93883a3 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -27,9 +27,19 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ "panel" ], metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index becd9bf6f31..847804842be 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -27,9 +27,19 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ "panel" ], metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 50c67ea58d0..42a9e4e71d0 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -13,9 +13,19 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ "panel" ], metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index 345e8c874de..50301b0cf1c 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -13,9 +13,19 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ "panel" ], metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index 406e20cfe55..8de370325aa 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -13,9 +13,19 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ "panel" ], metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 31fd0b94e96..855c14d6033 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -13,9 +13,19 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ "panel" ], metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index 85bc82a3136..c33398a6d33 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -13,9 +13,19 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ "panel" ], metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap new file mode 100644 index 00000000000..b9b4b15aa1f --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -0,0 +1,96 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "test", + responderAvatarIconUri: undefined, + welcomeMessage: undefined, + requests: [ + { + message: { + text: "@ChatProviderWithUsedContext test request", + parts: [ + { + kind: "agent", + range: { + start: 0, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 29 + }, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + metadata: { } + } + }, + { + range: { + start: 28, + endExclusive: 41 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 42 + }, + text: " test request", + kind: "text" + } + ] + }, + variableData: { variables: [ ] }, + response: [ ], + result: { metadata: { metadataKey: "value" } }, + followups: undefined, + isCanceled: false, + vote: undefined, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + metadata: { }, + slashCommands: [ ], + locations: [ "panel" ], + isDefault: undefined + }, + slashCommand: undefined, + usedContext: { + documents: [ + { + uri: { + scheme: "file", + authority: "", + path: "/test/path/to/file", + query: "", + fragment: "", + _formatted: null, + _fsPath: null + }, + version: 3, + ranges: [ + { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 2, + endColumn: 2 + } + ] + } + ], + kind: "usedContext" + }, + contentReferences: [ ] + } + ], + providerId: "testProvider" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap new file mode 100644 index 00000000000..75c5fa71f40 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap @@ -0,0 +1,9 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "test", + responderAvatarIconUri: undefined, + welcomeMessage: undefined, + requests: [ ], + providerId: "testProvider" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap new file mode 100644 index 00000000000..bad39b3eafc --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -0,0 +1,102 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "test", + responderAvatarIconUri: undefined, + welcomeMessage: undefined, + requests: [ + { + message: { + parts: [ + { + kind: "agent", + range: { + start: 0, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 29 + }, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + metadata: { + requester: { name: "test" }, + fullName: "test" + } + } + }, + { + range: { + start: 28, + endExclusive: 41 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 42 + }, + text: " test request", + kind: "text" + } + ], + text: "@ChatProviderWithUsedContext test request" + }, + variableData: { variables: [ ] }, + response: [ ], + result: { metadata: { metadataKey: "value" } }, + followups: undefined, + isCanceled: false, + vote: undefined, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + metadata: { + requester: { name: "test" }, + fullName: "test" + }, + slashCommands: [ ], + locations: [ "panel" ], + isDefault: undefined + }, + slashCommand: undefined, + usedContext: { + documents: [ + { + uri: { + scheme: "file", + authority: "", + path: "/test/path/to/file", + query: "", + fragment: "", + _formatted: null, + _fsPath: null + }, + version: 3, + ranges: [ + { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 2, + endColumn: 2 + } + ] + } + ], + kind: "usedContext" + }, + contentReferences: [ ] + } + ], + providerId: "testProvider" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap index d58da8b3744..cfed0a5d0ca 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap @@ -23,7 +23,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,10 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ], + locations: [ "panel" ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap index 0939983222f..75c5fa71f40 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap @@ -1,7 +1,7 @@ { - requesterUsername: "", + requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "", + responderUsername: "test", responderAvatarIconUri: undefined, welcomeMessage: undefined, requests: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap index 3a6b248a792..cb9c94b5d2d 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap @@ -22,7 +22,11 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { + description: undefined, + requester: { name: "test" }, + fullName: "test" + } } }, { @@ -54,7 +58,14 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { + description: undefined, + requester: { name: "test" }, + fullName: "test" + }, + slashCommands: [ ], + locations: [ "panel" ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownDecorationsRenderer.test.ts b/src/vs/workbench/contrib/chat/test/common/annotations.test.ts similarity index 95% rename from src/vs/workbench/contrib/chat/test/browser/chatMarkdownDecorationsRenderer.test.ts rename to src/vs/workbench/contrib/chat/test/common/annotations.test.ts index 1e3dd549432..e16117965c4 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownDecorationsRenderer.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/annotations.test.ts @@ -6,14 +6,14 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; +import { annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from '../../common/annotations'; function content(str: string): IChatMarkdownContent { return { kind: 'markdownContent', content: new MarkdownString(str) }; } -suite('ChatMarkdownDecorationsRenderer', function () { +suite('Annotations', function () { ensureNoDisposablesAreLeakedInTestSuite(); suite('extractVulnerabilitiesFromText', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index ffcda81f6de..3d63e2ed9d3 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -30,7 +30,7 @@ suite('ChatModel', () => { instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); - instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); }); test('Waits for initialization', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index ba397535bff..c28817bb090 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -9,13 +9,13 @@ 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 { ChatAgentLocation, 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'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ChatRequestParser', () => { @@ -31,7 +31,7 @@ suite('ChatRequestParser', () => { instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatService, new MockChatService()); - instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); varService = mockObject()({}); varService.getDynamicVariables.returns([]); @@ -112,12 +112,12 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return >{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => [], getLastSlashCommands: () => slashCommands }; + return { id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: { description: '' }, slashCommands }; }; test('agent with subcommand after text', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -127,7 +127,7 @@ suite('ChatRequestParser', () => { test('agents, subCommand', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -137,7 +137,7 @@ suite('ChatRequestParser', () => { test('agent with question mark', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -147,7 +147,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand with leading whitespace', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -157,7 +157,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand after newline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -167,7 +167,7 @@ suite('ChatRequestParser', () => { test('agent not first', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -177,7 +177,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); @@ -189,7 +189,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline, part2', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 3a2b6abc998..3125cf09d88 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -20,18 +20,18 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; 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'; 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 { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; class SimpleTestProvider extends Disposable implements IChatProvider { private static sessionId = 0; @@ -45,8 +45,6 @@ class SimpleTestProvider extends Disposable implements IChatProvider { async prepareSession(): Promise { return { id: SimpleTestProvider.sessionId++, - responderUsername: 'test', - requesterUsername: 'test', }; } @@ -58,14 +56,11 @@ class SimpleTestProvider extends Disposable implements IChatProvider { const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContext: IChatAgent = { id: chatAgentWithUsedContextId, + name: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, + locations: [ChatAgentLocation.Panel], metadata: {}, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands() { - return []; - }, + slashCommands: [], async invoke(request, progress, history, token) { progress({ documents: [ @@ -87,7 +82,7 @@ const chatAgentWithUsedContext: IChatAgent = { }, }; -suite('Chat', () => { +suite('ChatService', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); let storageService: IStorageService; @@ -110,24 +105,18 @@ suite('Chat', () => { instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); instantiationService.stub(IChatService, new MockChatService()); - chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); + chatAgentService = 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', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); + testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent)); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); }); test('retrieveSession', async () => { @@ -203,18 +192,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 +206,8 @@ suite('Chat', () => { }); test('can serialize', async () => { - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, 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 +227,7 @@ suite('Chat', () => { test('can deserialize', async () => { let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); // create the first service, send request, get response, and serialize the state { // serapate block to not leak variables in outer scope diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts new file mode 100644 index 00000000000..27a687cb24d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.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. + *--------------------------------------------------------------------------------------------*/ + +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; + +export class MockChatContributionService implements IChatContributionService { + _serviceBrand: undefined; + + constructor( + ) { } + + registeredProviders: IChatProviderContribution[] = []; + registerChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + + registerChatProvider(provider: IChatProviderContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatProvider(providerId: string): void { + throw new Error('Method not implemented.'); + } + getViewIdForProvider(providerId: string): string { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 35c17753392..d961bbb74c5 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatCompleteResponse, IChatDetail, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; @@ -60,9 +60,6 @@ export class MockChatService implements IChatService { addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void { throw new Error('Method not implemented.'); } - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void { - throw new Error('Method not implemented.'); - } getHistory(): IChatDetail[] { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index 4e34de6d72a..0df8e2b57e7 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/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; @@ -15,6 +15,10 @@ export class MockChatVariablesService implements IChatVariablesService { throw new Error('Method not implemented.'); } + getVariable(name: string): IChatVariableData | undefined { + throw new Error('Method not implemented.'); + } + hasVariable(name: string): boolean { throw new Error('Method not implemented.'); } @@ -32,4 +36,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/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index c85e0056d12..97e11f717ea 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -11,7 +11,7 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IChatAgent, IChatAgentCommand, IChatAgentHistoryEntry, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { 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,12 @@ 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]; + public readonly name: string; + constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { + this.name = id; + } 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 +50,24 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; - registerAgent(agent: IChatAgent): IDisposable { throw new Error(); } + registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { throw new Error(); } - getAgents(): Array { return agents; } - getAgent(id: string): IChatAgent | undefined { throw new Error(); } + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } + getActivatedAgents(): IChatAgent[] { return agents; } + getAgents(): IChatAgent[] { return agents; } 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(); } + registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); } + getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } + getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); } + updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); } } class TestSpeechService implements ISpeechService { _serviceBrand: undefined; - onDidRegisterSpeechProvider = Event.None; - onDidUnregisterSpeechProvider = Event.None; + onDidChangeHasSpeechProvider = Event.None; readonly hasSpeechProvider = true; readonly hasActiveSpeechToTextSession = false; @@ -74,7 +77,7 @@ suite('VoiceChat', () => { onDidStartSpeechToTextSession = Event.None; onDidEndSpeechToTextSession = Event.None; - createSpeechToTextSession(token: CancellationToken): ISpeechToTextSession { + async createSpeechToTextSession(token: CancellationToken): Promise { return { onDidChange: emitter.event }; @@ -91,10 +94,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 +113,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 +144,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 +171,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 +183,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 +196,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 +209,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 +222,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 +234,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 +247,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 +261,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 +283,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 +300,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 +313,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/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index 4225ffa7cd7..38fd5502ae2 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/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/src/vs/workbench/contrib/codeActions/browser/documentationContribution.ts b/src/vs/workbench/contrib/codeActions/browser/documentationContribution.ts index ed964662b2e..01f9be18e30 100644 --- a/src/vs/workbench/contrib/codeActions/browser/documentationContribution.ts +++ b/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/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index 842953efd0d..2a96931a42c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/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, 'editor'); disposables.add(session.onDidChange(e => { if (cts.token.isCancellationRequested) { return; diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 204ebdd1358..b47c9bc5642 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -8,9 +8,9 @@ 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/embeddedCodeEditorWidget'; +import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -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/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index fb540a708d1..ab46e943663 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/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/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index 557d9a025ec..dc43b9557b7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/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/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts index 4e8761ffb1d..3d9af339abe 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts +++ b/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/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts index 6acfd4495b7..b4f68265026 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 32ac5cb4dec..6fbc04ff214 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -11,7 +11,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { mixin } from 'vs/base/common/objects'; import { isMacintosh } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; @@ -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/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts similarity index 95% rename from src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts rename to src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts index f602d051103..e3d1fcd1064 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts @@ -47,7 +47,7 @@ interface IOnEnterRule { /** * Serialized form of a language configuration */ -interface ILanguageConfiguration { +export interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; autoClosingPairs?: Array; @@ -149,7 +149,7 @@ export class LanguageConfigurationFileHandler extends Disposable { } } - private _extractValidCommentRule(languageId: string, configuration: ILanguageConfiguration): CommentRule | undefined { + private static _extractValidCommentRule(languageId: string, configuration: ILanguageConfiguration): CommentRule | undefined { const source = configuration.comments; if (typeof source === 'undefined') { return undefined; @@ -179,7 +179,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidBrackets(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { + private static _extractValidBrackets(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { const source = configuration.brackets; if (typeof source === 'undefined') { return undefined; @@ -203,7 +203,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidAutoClosingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPairConditional[] | undefined { + private static _extractValidAutoClosingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPairConditional[] | undefined { const source = configuration.autoClosingPairs; if (typeof source === 'undefined') { return undefined; @@ -249,7 +249,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidSurroundingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPair[] | undefined { + private static _extractValidSurroundingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPair[] | undefined { const source = configuration.surroundingPairs; if (typeof source === 'undefined') { return undefined; @@ -289,7 +289,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidColorizedBracketPairs(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { + private static _extractValidColorizedBracketPairs(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { const source = configuration.colorizedBracketPairs; if (typeof source === 'undefined') { return undefined; @@ -312,7 +312,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidOnEnterRules(languageId: string, configuration: ILanguageConfiguration): OnEnterRule[] | undefined { + private static _extractValidOnEnterRules(languageId: string, configuration: ILanguageConfiguration): OnEnterRule[] | undefined { const source = configuration.onEnterRules; if (typeof source === 'undefined') { return undefined; @@ -385,7 +385,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _handleConfig(languageId: string, configuration: ILanguageConfiguration): void { + public static extractValidConfig(languageId: string, configuration: ILanguageConfiguration): ExplicitLanguageConfiguration { const comments = this._extractValidCommentRule(languageId, configuration); const brackets = this._extractValidBrackets(languageId, configuration); @@ -421,11 +421,15 @@ export class LanguageConfigurationFileHandler extends Disposable { folding, __electricCharacterSupport: undefined, }; + return richEditConfig; + } + private _handleConfig(languageId: string, configuration: ILanguageConfiguration): void { + const richEditConfig = LanguageConfigurationFileHandler.extractValidConfig(languageId, configuration); this._languageConfigurationService.register(languageId, richEditConfig, 50); } - private _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined { + private static _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined { if (typeof value === 'string') { try { return new RegExp(value, ''); @@ -454,7 +458,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return undefined; } - private _mapIndentationRules(languageId: string, indentationRules: IIndentationRules): IndentationRule | undefined { + private static _mapIndentationRules(languageId: string, indentationRules: IIndentationRules): IndentationRule | undefined { const increaseIndentPattern = this._parseRegex(languageId, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern); if (!increaseIndentPattern) { return undefined; diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts new file mode 100644 index 00000000000..6d58f57ebba --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -0,0 +1,326 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { getReindentEditOperations } from 'vs/editor/contrib/indentation/common/indentation'; +import { IRelaxedTextModelCreationOptions, createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageConfiguration, LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; +import { parse } from 'vs/base/common/json'; +import { IRange } from 'vs/editor/common/core/range'; + +function getIRange(range: IRange): IRange { + return { + startLineNumber: range.startLineNumber, + startColumn: range.startColumn, + endLineNumber: range.endLineNumber, + endColumn: range.endColumn + }; +} + +suite('Auto-Reindentation - TypeScript/JavaScript', () => { + + const languageId = 'ts-test'; + const options: IRelaxedTextModelCreationOptions = {}; + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createModelServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const configPath = path.join('extensions', 'typescript-basics', 'language-configuration.json'); + const configString = fs.readFileSync(configPath).toString(); + const config = parse(configString, []); + const configParsed = LanguageConfigurationFileHandler.extractValidConfig(languageId, config); + disposables.add(languageConfigurationService.register(languageId, configParsed)); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + // Test which can be ran to find cases of incorrect indentation... + test.skip('Find Cases of Incorrect Indentation', () => { + + const filePath = path.join('..', 'TypeScript', 'src', 'server', 'utilities.ts'); + const fileContents = fs.readFileSync(filePath).toString(); + + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + model.applyEdits(editOperations); + + // save the files to disk + const initialFile = path.join('..', 'autoindent', 'initial.ts'); + const finalFile = path.join('..', 'autoindent', 'final.ts'); + fs.writeFileSync(initialFile, fileContents); + fs.writeFileSync(finalFile, model.getValue()); + }); + + // Unit tests for increase and decrease indent patterns... + + /** + * First increase indent and decrease indent patterns: + * + * - decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/ + * - In (https://macromates.com/manual/en/appendix) + * Either we have white space before the closing bracket, or we have a multi line comment ending on that line followed by whitespaces + * This is followed by any character. + * Textmate decrease indent pattern is as follows: /^(.*\*\/)?\s*\}[;\s]*$/ + * Presumably allowing multi line comments ending on that line implies that } is itself not part of a multi line comment + * + * - increaseIndentPattern: /^.*\{[^}"']*$/ + * - In (https://macromates.com/manual/en/appendix) + * This regex means that we increase the indent when we have any characters followed by the opening brace, followed by characters + * except for closing brace }, double quotes " or single quote '. + * The } is checked in order to avoid the indentation in the following case `int arr[] = { 1, 2, 3 };` + * The double quote and single quote are checked in order to avoid the indentation in the following case: str = "foo {"; + */ + + test('Issue #25437', () => { + // issue: https://github.com/microsoft/vscode/issues/25437 + // fix: https://github.com/microsoft/vscode/commit/8c82a6c6158574e098561c28d470711f1b484fc8 + // explanation: var foo = `{`; should not increase indentation + + // increaseIndentPattern: /^.*\{[^}"']*$/ -> /^.*\{[^}"'`]*$/ + + const fileContents = [ + 'const foo = `{`;', + ' ', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + const operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 5, + }); + assert.deepStrictEqual(operation.text, ''); + }); + + test('Enriching the hover', () => { + // issue: - + // fix: https://github.com/microsoft/vscode/commit/19ae0932c45b1096443a8c1335cf1e02eb99e16d + // explanation: + // - decrease indent on ) and ] also + // - increase indent on ( and [ also + + // decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/ -> /^(.*\*\/)?\s*[\}\]\)].*$/ + // increaseIndentPattern: /^.*\{[^}"'`]*$/ -> /^.*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ + + let fileContents = [ + 'function foo(', + ' bar: string', + ' ){}', + ].join('\n'); + let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + let operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 3, + "startColumn": 1, + "endLineNumber": 3, + "endColumn": 5, + }); + assert.deepStrictEqual(operation.text, ''); + + fileContents = [ + 'function foo(', + 'bar: string', + '){}', + ].join('\n'); + model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 1, + }); + assert.deepStrictEqual(operation.text, ' '); + }); + + test('Issue #86176', () => { + // issue: https://github.com/microsoft/vscode/issues/86176 + // fix: https://github.com/microsoft/vscode/commit/d89e2e17a5d1ba37c99b1d3929eb6180a5bfc7a8 + // explanation: When quotation marks are present on the first line of an if statement or for loop, following line should not be indented + + // increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/ + // explanation: after open brace, do not decrease indent if it is followed on the same line by " // " + // todo@aiday-mar: should also apply for when it follows ( and [ + + const fileContents = [ + `if () { // '`, + `x = 4`, + `}` + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + const operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 1, + }); + assert.deepStrictEqual(operation.text, ' '); + }); + + test('Issue #141816', () => { + + // issue: https://github.com/microsoft/vscode/issues/141816 + // fix: https://github.com/microsoft/vscode/pull/141997/files + // explanation: if (, [, {, is followed by a forward slash then assume we are in a regex pattern, and do not indent + + // increaseIndentPattern: /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/ + // -> Final current increase indent pattern at of writing + + const fileContents = [ + 'const r = /{/;', + ' ', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + const operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 4, + }); + assert.deepStrictEqual(operation.text, ''); + }); + + test('Issue #29886', () => { + // issue: https://github.com/microsoft/vscode/issues/29886 + // fix: https://github.com/microsoft/vscode/commit/7910b3d7bab8a721aae98dc05af0b5e1ea9d9782 + + // decreaseIndentPattern: /^(.*\*\/)?\s*[\}\]\)].*$/ -> /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/ + // -> Final current decrease indent pattern at the time of writing + + // explanation: Positive lookahead: (?= «pattern») matches if pattern matches what comes after the current location in the input string. + // Negative lookahead: (?! «pattern») matches if pattern does not match what comes after the current location in the input string + // The change proposed is to not decrease the indent if there is a multi-line comment ending on the same line before the closing parentheses + + const fileContents = [ + 'function foo() {', + ' bar(/* */)', + '};', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + // Failing tests inferred from the current regexes... + + test.skip('Incorrect deindentation after `*/}` string', () => { + + // explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent + // Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent + + const fileContents = [ + `const obj = {`, + ` obj1: {`, + ` brace : '*/}'`, + ` }`, + `}`, + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + // Failing tests from issues... + + test.skip('Issue #56275', () => { + + // issue: https://github.com/microsoft/vscode/issues/56275 + // explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent + // Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent + + let fileContents = [ + 'function foo() {', + ' var bar = (/b*/);', + '}', + ].join('\n'); + let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + + fileContents = [ + 'function foo() {', + ' var bar = "/b*/)";', + '}', + ].join('\n'); + model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + test.skip('Issue #116843', () => { + + // issue: https://github.com/microsoft/vscode/issues/116843 + // related: https://github.com/microsoft/vscode/issues/43244 + // explanation: When you have an arrow function, you don't have { or }, but you would expect indentation to still be done in that way + + // TODO: requires exploring indent/outdent pairs instead + + const fileContents = [ + 'const add1 = (n) =>', + ' n + 1;', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + test.skip('Issue #185252', () => { + + // issue: https://github.com/microsoft/vscode/issues/185252 + // explanation: Reindenting the comment correctly + + const fileContents = [ + '/*', + ' * This is a comment.', + ' */', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + test.skip('Issue 43244: incorrect indentation when signature of function call spans several lines', () => { + + // issue: https://github.com/microsoft/vscode/issues/43244 + + const fileContents = [ + 'function callSomeOtherFunction(one: number, two: number) { }', + 'function someFunction() {', + ' callSomeOtherFunction(4,', + ' 5)', + '}', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); +}); diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 7085518f94a..df2afb078e5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -9,7 +9,7 @@ import * as languages from 'vs/editor/common/languages'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IActionRunner, IAction, Separator, ActionRunner } from 'vs/base/common/actions'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -30,7 +30,7 @@ import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -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, @@ -301,21 +309,22 @@ export class CommentNode extends Disposable { private createToolbar() { this.toolbar = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action.id === ToggleReactionsAction.ID) { return new DropdownMenuActionViewItem( action, (action).menuActions, this.contextMenuService, { - actionViewItemProvider: action => this.actionViewItemProvider(action as Action), + ...options, + actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options), actionRunner: this.actionRunner, classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)], anchorAlignmentProvider: () => AnchorAlignment.RIGHT } ); } - return this.actionViewItemProvider(action as Action); + return this.actionViewItemProvider(action as Action, options); }, orientation: ActionsOrientation.HORIZONTAL }); @@ -357,8 +366,7 @@ export class CommentNode extends Disposable { } } - actionViewItemProvider(action: Action) { - let options = {}; + actionViewItemProvider(action: Action, options: IActionViewItemOptions) { if (action.id === ToggleReactionsAction.ID) { options = { label: false, icon: true }; } else { @@ -369,9 +377,9 @@ export class CommentNode extends Disposable { const item = new ReactionActionViewItem(action); return item; } else if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, undefined); + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, options); } else { const item = new ActionViewItem({}, action, options); return item; @@ -413,11 +421,11 @@ export class CommentNode extends Disposable { (toggleReactionAction).menuActions, this.contextMenuService, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action.id === ToggleReactionsAction.ID) { return toggleReactionActionViewItem; } - return this.actionViewItemProvider(action as Action); + return this.actionViewItemProvider(action as Action, options); }, actionRunner: this.actionRunner, classNames: 'toolbar-toggle-pickReactions', @@ -431,21 +439,21 @@ export class CommentNode extends Disposable { private createReactionsContainer(commentDetailsContainer: HTMLElement): void { this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions')); this._reactionsActionBar = new ActionBar(this._reactionActionsContainer, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action.id === ToggleReactionsAction.ID) { return new DropdownMenuActionViewItem( action, (action).menuActions, this.contextMenuService, { - actionViewItemProvider: action => this.actionViewItemProvider(action as Action), + actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options), actionRunner: this.actionRunner, classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)], anchorAlignmentProvider: () => AnchorAlignment.RIGHT } ); } - return this.actionViewItemProvider(action as Action); + return this.actionViewItemProvider(action as Action, options); } }); this._register(this._reactionsActionBar); @@ -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/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 21aeb3f22d5..55ea0d4e8f9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -30,6 +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/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const COMMENT_SCHEME = 'comment'; let INMEM_MODEL_ID = 0; @@ -355,7 +357,7 @@ export class CommentReply extends Disposable { private createReplyButton(commentEditor: ICodeEditor, commentForm: HTMLElement) { this._reviewThreadReplyButton = dom.append(commentForm, dom.$(`button.review-thread-reply-button.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); - this._reviewThreadReplyButton.title = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._reviewThreadReplyButton, this._commentOptions?.prompt || nls.localize('reply', "Reply..."))); this._reviewThreadReplyButton.textContent = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); // bind click/escape actions for reviewThreadReplyButton and textArea diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 1072a60aedf..accc000bdce 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/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/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index b206d26b011..9784625cd2f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/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/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index bcd9366e524..e09cd4f5167 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/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/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index e5ae9040d50..4696f822eca 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/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,23 @@ 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; } } + const rangeToReveal = this._commentThread.range + ? new Range(this._commentThread.range.startLineNumber, this._commentThread.range.startColumn, this._commentThread.range.endLineNumber + 1, 1) + : new Range(1, 1, 1, 1); - this.editor.revealRangeInCenter(this._commentThread.range ?? new Range(1, 1, 1, 1)); - if (focus) { + this.editor.revealRangeInCenter(rangeToReveal); + if (focus === CommentWidgetFocus.Widget) { this._commentThreadWidget.focus(); + } else if (focus === CommentWidgetFocus.Editor) { + this._commentThreadWidget.focusCommentEditor(); } } @@ -229,7 +242,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 +271,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 +294,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/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index b47cdb2b883..e57e7c315e2 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/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/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index d2fa2c78e8a..b471cf60732 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/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,9 +23,9 @@ 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 { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +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'; import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; @@ -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 | undefined, 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 | undefined, 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/src/vs/workbench/contrib/comments/browser/commentsModel.ts b/src/vs/workbench/contrib/comments/browser/commentsModel.ts index 6d345350e83..d0701d5f344 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsModel.ts +++ b/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/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 69ffc3d3f6d..1c0c588d215 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/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,6 +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/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'; @@ -45,6 +56,7 @@ interface IResourceTemplateData { interface ICommentThreadTemplateData { threadMetadata: { + relevance: HTMLElement; icon: HTMLElement; userNames: HTMLSpanElement; timestamp: TimestampWidget; @@ -60,6 +72,7 @@ interface ICommentThreadTemplateData { separator: HTMLElement; timestamp: TimestampWidget; }; + actionBar: ActionBar; disposables: IDisposable[]; } @@ -122,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, @@ -156,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 { @@ -196,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))); @@ -219,7 +300,7 @@ export class CommentNodeRenderer implements IListRenderer const renderedComment = this.getRenderedComment(originalComment.comment.body, disposables); templateData.disposables.push(renderedComment); templateData.threadMetadata.commentPreview.appendChild(renderedComment.element.firstElementChild ?? renderedComment.element); - templateData.threadMetadata.commentPreview.title = renderedComment.element.textContent ?? ''; + templateData.disposables.push(setupCustomHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? '')); } if (node.element.range) { @@ -230,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; @@ -248,6 +337,7 @@ export class CommentNodeRenderer implements IListRenderer disposeTemplate(templateData: ICommentThreadTemplateData): void { templateData.disposables.forEach(disposeable => disposeable.dispose()); + templateData.actionBar.dispose(); } } @@ -345,6 +435,8 @@ export class Filter implements ITreeFilter { + private readonly menus: CommentsMenus; + constructor( labels: ResourceLabels, container: HTMLElement, @@ -353,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/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 385ac16e1dc..5642f58d2fa 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/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 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; + const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].thread : element.thread; + const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment : undefined; + return revealCommentThread(this.commentService, this.editorService, this.uriIdentityService, threadToReveal, commentToReveal, false, pinned, preserveFocus, sideBySide); } private async refresh(): Promise { diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index e6fd43f4b91..7a0f4d21531 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -123,7 +123,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleUnResolvedComments`, - title: localize('toggle unresolved', "Toggle Unresolved Comments"), + title: localize('toggle unresolved', "Show Unresolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_UNRESOLVED, @@ -148,7 +148,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleResolvedComments`, - title: localize('toggle resolved', "Toggle Resolved Comments"), + title: localize('toggle resolved', "Show Resolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_RESOLVED, diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index a349ec52490..938c658fd2d 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/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/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index 2a4f2c3a2bb..b5b62904843 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -6,7 +6,7 @@ import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { EditorAction, EditorContributionInstantiation, EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -123,7 +123,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { export function calculateEditorHeight(parentEditor: LayoutableEditor, editor: ICodeEditor, currentHeight: number): number { const layoutInfo = editor.getLayoutInfo(); const lineHeight = editor.getOption(EditorOption.lineHeight); - const contentHeight = (editor.getModel()?.getLineCount()! * lineHeight) ?? editor.getContentHeight(); // Can't just call getContentHeight() because it returns an incorrect, large, value when the editor is first created. + const contentHeight = (editor._getViewModel()?.getLineCount()! * lineHeight) ?? editor.getContentHeight(); // Can't just call getContentHeight() because it returns an incorrect, large, value when the editor is first created. if ((contentHeight > layoutInfo.height) || (contentHeight < layoutInfo.height && currentHeight > MIN_EDITOR_HEIGHT)) { const linesToAdd = Math.ceil((contentHeight - layoutInfo.height) / lineHeight); diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index 2b9f79d4a88..dbfad43dfd0 100644 --- a/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 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 { fromNow } from 'vs/base/common/date'; import { Disposable } from 'vs/base/common/lifecycle'; import { language } from 'vs/base/common/platform'; @@ -15,11 +17,14 @@ export class TimestampWidget extends Disposable { private _timestamp: Date | undefined; private _useRelativeTime: boolean; + private hover: ICustomHover; + constructor(private configurationService: IConfigurationService, container: HTMLElement, timeStamp?: Date) { super(); this._date = dom.append(container, dom.$('span.timestamp')); this._date.style.display = 'none'; this._useRelativeTime = this.useRelativeTimeSetting; + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._date, '')); this.setTimestamp(timeStamp); } @@ -52,9 +57,7 @@ export class TimestampWidget extends Disposable { } this._date.textContent = textContent; - if (tooltip) { - this._date.title = tooltip; - } + this.hover.update(tooltip ?? ''); } } diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index 9a6d8786372..fbf25f6c06b 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/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/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index a3e171b9d40..cd5f0ddf60c 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/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/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 4e677142d69..214f354088e 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/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,23 @@ 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 { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; 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 +91,10 @@ 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, + @ICustomEditorLabelService private readonly customEditorLabelService: ICustomEditorLabelService, ) { super({ providedId: init.viewType, viewType: init.viewType, name: '' }, webview, webviewWorkbenchService); this._editorResource = init.resource; @@ -101,6 +112,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); + this._register(this.customEditorLabelService.onDidChange(() => this.updateLabel())); } private onLabelEvent(scheme: string): void { @@ -112,6 +124,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { private updateLabel(): void { // Clear any cached labels from before + this._editorName = undefined; this._shortDescription = undefined; this._mediumDescription = undefined; this._longDescription = undefined; @@ -135,7 +148,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; @@ -158,8 +170,13 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return capabilities; } + private _editorName: string | undefined = undefined; override getName(): string { - return basename(this.labelService.getUriLabel(this.resource)); + if (typeof this._editorName !== 'string') { + this._editorName = this.customEditorLabelService.getName(this.resource) ?? basename(this.labelService.getUriLabel(this.resource)); + } + + return this._editorName; } override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { @@ -389,4 +406,51 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } }; } + + public override claim(claimant: unknown, targetWindow: CodeWindow, scopedContextKeyService: IContextKeyService | undefined): void { + if (this.doCanMove(targetWindow.vscodeWindowId) !== true) { + 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(sourceGroup: GroupIdentifier, targetGroup: GroupIdentifier): true | string { + const resolvedTargetGroup = this.editorGroupsService.getGroup(targetGroup); + if (resolvedTargetGroup) { + const canMove = this.doCanMove(resolvedTargetGroup.windowId); + if (typeof canMove === 'string') { + return canMove; + } + } + + return super.canMove(sourceGroup, targetGroup); + } + + private doCanMove(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 '{0}': The editor contains changes that can only be saved in its current window.", this.getName()); + } + } + + return true; + } } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 40c47ab8a80..28efa3bf905 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/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/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts index cb0defd952f..e4aa463c98d 100644 --- a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/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/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index ac626b82f40..fe763dc159a 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -7,6 +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/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'; @@ -187,19 +189,18 @@ export abstract class AbstractExpressionsRenderer implements IT abstract get templateId(): string; renderTemplate(container: HTMLElement): IExpressionTemplateData { + const templateDisposable = new DisposableStore(); const expression = dom.append(container, $('.expression')); const name = dom.append(expression, $('span.name')); const lazyButton = dom.append(expression, $('span.lazy-button')); lazyButton.classList.add(...ThemeIcon.asClassNameArray(Codicon.eye)); - lazyButton.title = localize('debug.lazyButton.tooltip', "Click to expand"); + 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')); - const templateDisposable = new DisposableStore(); - let actionBar: ActionBar | undefined; if (this.renderActionBar) { dom.append(expression, $('.span.actionbar-spacer')); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1c36e83c55c..1b53d40047f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -54,7 +54,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = { description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), glyphMargin: { position: GlyphMarginLane.Right }, - glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint.")), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint")), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; @@ -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/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 26ae95ccf1d..59a3a4dd4bf 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -16,7 +16,7 @@ import 'vs/css!./media/breakpointWidget'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand, ServicesAccessor, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -265,30 +265,28 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi private createTriggerBreakpointInput(container: HTMLElement) { const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint); + const breakpointOptions: ISelectOptionItem[] = [ + { text: nls.localize('noTriggerByBreakpoint', 'None'), isDisabled: true }, + ...breakpoints.map(bp => ({ + text: `${this.labelService.getUriLabel(bp.uri, { relative: true })}: ${bp.lineNumber}`, + description: nls.localize('triggerByLoading', 'Loading...') + })), + ]; const index = breakpoints.findIndex((bp) => this.breakpoint?.triggeredBy === bp.getId()); - let select = 0; - if (index > -1) { - select = index + 1; - } - - Promise.all(breakpoints.map(async (bp): Promise => ({ - text: `${this.labelService.getUriLabel(bp.uri, { relative: true })}: ${bp.lineNumber}`, - description: await this.textModelService.createModelReference(bp.uri).then(ref => { + for (const [i, bp] of breakpoints.entries()) { + this.textModelService.createModelReference(bp.uri).then(ref => { try { - return ref.object.textEditorModel.getLineContent(bp.lineNumber).trim(); + breakpointOptions[i + 1].description = ref.object.textEditorModel.getLineContent(bp.lineNumber).trim(); } finally { ref.dispose(); } - }, () => undefined), - }))).then(breakpoints => { - selectBreakpointBox.setOptions([ - { text: nls.localize('noTriggerByBreakpoint', 'None') }, - ...breakpoints - ], select); - }); + }).catch(() => { + breakpointOptions[i + 1].description = nls.localize('noBpSource', 'Could not load source.'); + }); + } - const selectBreakpointBox = this.selectBreakpointBox = new SelectBox([{ text: nls.localize('triggerByLoading', 'Loading...'), isDisabled: true }], 0, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('selectBreakpoint', 'Select breakpoint') }); + const selectBreakpointBox = this.selectBreakpointBox = new SelectBox(breakpointOptions, index + 1, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('selectBreakpoint', 'Select breakpoint') }); selectBreakpointBox.onDidSelect(e => { if (e.index === 0) { this.triggeredByBreakpointInput = undefined; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index e60c8079a14..ddfb63625fd 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -8,7 +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/hoverDelegateFactory'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +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'; @@ -49,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.$; @@ -85,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; @@ -118,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); @@ -140,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), ], { @@ -264,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'); @@ -549,7 +555,7 @@ class BreakpointsRenderer implements IListRenderer, private breakpointSupportsCondition: IContextKey, private breakpointItemType: IContextKey, + private breakpointIsDataBytes: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -780,9 +788,9 @@ 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 { @@ -869,12 +879,12 @@ class InstructionBreakpointsRenderer implements IListRenderer { + const debugService = accessor.get(IDebugService); + const session = debugService.getViewModel().focusedSession; + if (!session) { + return; + } + + let defaultValue = undefined; + if (existingBreakpoint && existingBreakpoint.src.type === DataBreakpointSetType.Address) { + defaultValue = `${existingBreakpoint.src.address} + ${existingBreakpoint.src.bytes}`; + } + + const quickInput = accessor.get(IQuickInputService); + const notifications = accessor.get(INotificationService); + const range = await this.getRange(quickInput, defaultValue); + if (!range) { + return; + } + + let info: IDataBreakpointInfoResponse | undefined; + try { + info = await session.dataBytesBreakpointInfo(range.address, range.bytes); + } catch (e) { + notifications.error(localize('dataBreakpointError', "Failed to set data breakpoint at {0}: {1}", range.address, e.message)); + } + + if (!info?.dataId) { + return; + } + + let accessType: DebugProtocol.DataBreakpointAccessType = 'write'; + if (info.accessTypes && info.accessTypes?.length > 1) { + const accessTypes = info.accessTypes.map(type => ({ label: type })); + const selectedAccessType = await quickInput.pick(accessTypes, { placeHolder: localize('dataBreakpointAccessType', "Select the access type to monitor") }); + if (!selectedAccessType) { + return; + } + + accessType = selectedAccessType.label; + } + + const src: DataBreakpointSource = { type: DataBreakpointSetType.Address, ...range }; + if (existingBreakpoint) { + await debugService.removeDataBreakpoints(existingBreakpoint.getId()); + } + + await debugService.addDataBreakpoint({ + description: info.description, + src, + canPersist: true, + accessTypes: info.accessTypes, + accessType: accessType, + initialSessionData: { session, dataId: info.dataId } + }); + } + + private getRange(quickInput: IQuickInputService, defaultValue?: string) { + return new Promise<{ address: string; bytes: number } | undefined>(resolve => { + const input = quickInput.createInputBox(); + input.prompt = localize('dataBreakpointMemoryRangePrompt', "Enter a memory range in which to break"); + input.placeholder = localize('dataBreakpointMemoryRangePlaceholder', 'Absolute range (0x1234 - 0x1300) or range of bytes after an address (0x1234 + 0xff)'); + if (defaultValue) { + input.value = defaultValue; + input.valueSelection = [0, defaultValue.length]; + } + input.onDidChangeValue(e => { + const err = this.parseAddress(e, false); + input.validationMessage = err?.error; + }); + input.onDidAccept(() => { + const r = this.parseAddress(input.value, true); + if ('error' in r) { + input.validationMessage = r.error; + } else { + resolve(r); + } + input.dispose(); + }); + input.onDidHide(() => { + resolve(undefined); + input.dispose(); + }); + input.ignoreFocusOut = true; + input.show(); + }); + } + + private parseAddress(range: string, isFinal: false): { error: string } | undefined; + private parseAddress(range: string, isFinal: true): { error: string } | { address: string; bytes: number }; + private parseAddress(range: string, isFinal: boolean): { error: string } | { address: string; bytes: number } | undefined { + const parts = /^(\S+)\s*(?:([+-])\s*(\S+))?/.exec(range); + if (!parts) { + return { error: localize('dataBreakpointAddrFormat', 'Address should be a range of numbers the form "[Start] - [End]" or "[Start] + [Bytes]"') }; + } + + const isNum = (e: string) => isFinal ? /^0x[0-9a-f]*|[0-9]*$/i.test(e) : /^0x[0-9a-f]+|[0-9]+$/i.test(e); + const [, startStr, sign = '+', endStr = '1'] = parts; + + for (const n of [startStr, endStr]) { + if (!isNum(n)) { + return { error: localize('dataBreakpointAddrStartEnd', 'Number must be a decimal integer or hex value starting with \"0x\", got {0}', n) }; + } + } + + if (!isFinal) { + return; + } + + const start = BigInt(startStr); + const end = BigInt(endStr); + const address = `0x${start.toString(16)}`; + if (sign === '-') { + return { address, bytes: Number(start - end) }; + } + + return { address, bytes: Number(end) }; + } +} + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.addDataBreakpointOnAddress', + title: { + ...localize2('addDataBreakpointOnAddress', "Add Data Breakpoint at Address"), + mnemonicTitle: localize({ key: 'miDataBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Data Breakpoint..."), + }, + f1: true, + icon: icons.watchExpressionsAddDataBreakpoint, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 11, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, ContextKeyExpr.equals('view', BREAKPOINTS_VIEW_ID)) + }, { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED + }] + }); + } +}); + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.editDataBreakpointOnAddress', + title: localize2('editDataBreakpointOnAddress', "Edit Address..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES), + group: 'navigation', + order: 15, + }] + }); + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 4145d4ae91a..db948476aef 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -48,6 +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/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = dom.$; @@ -133,6 +135,7 @@ async function expandTo(session: IDebugSession, tree: WorkbenchCompressibleAsync export class CallStackView extends ViewPane { private stateMessage!: HTMLSpanElement; private stateMessageLabel!: HTMLSpanElement; + private stateMessageLabelHover!: ICustomHover; private onCallStackChangeScheduler: RunOnceScheduler; private needsRefresh = false; private ignoreSelectionChangedEvent = false; @@ -172,12 +175,12 @@ export class CallStackView extends ViewPane { const stoppedDetails = sessions.length === 1 ? sessions[0].getStoppedDetails() : undefined; if (stoppedDetails && (thread || typeof stoppedDetails.threadId !== 'number')) { this.stateMessageLabel.textContent = stoppedDescription(stoppedDetails); - this.stateMessageLabel.title = stoppedText(stoppedDetails); + this.stateMessageLabelHover.update(stoppedText(stoppedDetails)); this.stateMessageLabel.classList.toggle('exception', stoppedDetails.reason === 'exception'); this.stateMessage.hidden = false; } else if (sessions.length === 1 && sessions[0].state === State.Running) { this.stateMessageLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running"); - this.stateMessageLabel.title = sessions[0].getLabel(); + this.stateMessageLabelHover.update(sessions[0].getLabel()); this.stateMessageLabel.classList.remove('exception'); this.stateMessage.hidden = false; } else { @@ -216,6 +219,7 @@ export class CallStackView extends ViewPane { this.stateMessage = dom.append(container, $('span.call-stack-state-message')); this.stateMessage.hidden = true; this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); + this.stateMessageLabelHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.stateMessage, '')); } protected override renderBody(container: HTMLElement): void { @@ -344,6 +348,7 @@ export class CallStackView extends ViewPane { } if (!this.isBodyVisible()) { this.needsRefresh = true; + this.selectionNeedsUpdate = true; return; } if (this.onCallStackChangeScheduler.isScheduled()) { @@ -493,6 +498,7 @@ interface ISessionTemplateData { interface IErrorTemplateData { label: HTMLElement; + templateDisposable: DisposableStore; } interface ILabelTemplateData { @@ -506,7 +512,7 @@ interface IStackFrameTemplateData { lineNumber: HTMLElement; label: HighlightedLabel; actionBar: ActionBar; - templateDisposable: IDisposable; + templateDisposable: DisposableStore; } function getSessionContextOverlay(session: IDebugSession): [string, any][] { @@ -536,8 +542,8 @@ class SessionsRenderer implements ICompressibleTreeRenderer t.stopped); @@ -603,11 +609,11 @@ class SessionsRenderer implements ICompressibleTreeRenderer, _index: number, data: IThreadTemplateData): void { const thread = element.element; - data.thread.title = thread.name; + data.elementDisposable.add(setupCustomHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name)); data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception'); @@ -727,9 +733,10 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, index: number, data: IErrorTemplateData): void { const error = element.element; data.label.textContent = error; - data.label.title = error; + data.templateDisposable.add(setupCustomHover(getDefaultHoverDelegate('mouse'), data.label, error)); } renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IErrorTemplateData, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 8cc98fe8751..5694741d995 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -450,6 +450,18 @@ configurationRegistry.registerConfiguration({ description: nls.localize({ comment: ['This is the description for a setting'], key: 'allowBreakpointsEverywhere' }, "Allow setting breakpoints in any file."), default: false }, + 'debug.gutterMiddleClickAction': { + type: 'string', + enum: ['logpoint', 'conditionalBreakpoint', 'triggeredBreakpoint', 'none'], + description: nls.localize({ comment: ['This is the description for a setting'], key: 'gutterMiddleClickAction' }, 'Controls the action to perform when clicking the editor gutter with the middle mouse button.'), + enumDescriptions: [ + nls.localize('debug.gutterMiddleClickAction.logpoint', "Add Logpoint."), + nls.localize('debug.gutterMiddleClickAction.conditionalBreakpoint', "Add Conditional Breakpoint."), + nls.localize('debug.gutterMiddleClickAction.triggeredBreakpoint', "Add Triggered Breakpoint."), + nls.localize('debug.gutterMiddleClickAction.none', "Don't perform any action."), + ], + default: 'logpoint', + }, 'debug.openExplorerOnEnd': { type: 'boolean', description: nls.localize({ comment: ['This is the description for a setting'], key: 'openExplorerOnEnd' }, "Automatically open the explorer view at the end of a debug session."), diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 433a562e4bf..ccc284c0724 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -22,6 +22,8 @@ import { BaseActionViewItem, IBaseActionViewItemOptions, SelectActionViewItem } import { debugStart } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { defaultSelectBoxStyles } 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.$; @@ -74,9 +76,10 @@ export class StartDebugActionViewItem extends BaseActionViewItem { this.start = dom.append(container, $(ThemeIcon.asCSSSelector(debugStart))); const keybinding = this.keybindingService.lookupKeybinding(this.action.id)?.getLabel(); const keybindingLabel = keybinding ? ` (${keybinding})` : ''; - this.start.title = this.action.label + keybindingLabel; + const title = this.action.label + keybindingLabel; + this.toDispose.push(setupCustomHover(getDefaultHoverDelegate('mouse'), this.start, title)); this.start.setAttribute('role', 'button'); - this.start.ariaLabel = this.start.title; + this.start.ariaLabel = title; this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.CLICK, () => { this.start.blur(); diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index 90be002dfc7..1af9c0b359b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -321,7 +321,7 @@ export function registerColors() { const debugIconDisconnectColor = theme.getColor(debugIconDisconnectForeground); if (debugIconDisconnectColor) { - collector.addRule(`.monaco-workbench .part > .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 .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .command-center-center ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor}; }`); } const debugIconRestartColor = theme.getColor(debugIconRestartForeground); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 3bb555a1a53..f2f839ff912 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -237,7 +237,7 @@ async function goToBottomOfCallStack(debugService: IDebugService) { if (callStack.length > 0) { const nextVisibleFrame = findNextVisibleFrame(false, callStack, 0); // must consider the next frame up first, which will be the last frame if (nextVisibleFrame) { - debugService.focusStackFrame(nextVisibleFrame); + debugService.focusStackFrame(nextVisibleFrame, undefined, undefined, { preserveFocus: false }); } } } @@ -247,7 +247,7 @@ function goToTopOfCallStack(debugService: IDebugService) { const thread = debugService.getViewModel().focusedThread; if (thread) { - debugService.focusStackFrame(thread.getTopStackFrame()); + debugService.focusStackFrame(thread.getTopStackFrame(), undefined, undefined, { preserveFocus: false }); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index a800e241511..30789475e42 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/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/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index b1a9a4a0789..12376d1a83f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -79,6 +79,7 @@ export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove- export const watchExpressionRemove = registerIcon('watch-expression-remove', Codicon.removeClose, localize('watchExpressionRemove', 'Icon for the Remove action in the watch view.')); export const watchExpressionsAdd = registerIcon('watch-expressions-add', Codicon.add, localize('watchExpressionsAdd', 'Icon for the add action in the watch view.')); export const watchExpressionsAddFuncBreakpoint = registerIcon('watch-expressions-add-function-breakpoint', Codicon.add, localize('watchExpressionsAddFuncBreakpoint', 'Icon for the add function breakpoint action in the watch view.')); +export const watchExpressionsAddDataBreakpoint = registerIcon('watch-expressions-add-data-breakpoint', Codicon.variableGroup, localize('watchExpressionsAddDataBreakpoint', 'Icon for the add data breakpoint action in the breakpoints view.')); export const breakpointsRemoveAll = registerIcon('breakpoints-remove-all', Codicon.closeAll, localize('breakpointsRemoveAll', 'Icon for the Remove All action in the breakpoints view.')); export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon.activateBreakpoints, localize('breakpointsActivate', 'Icon for the activate action in the breakpoints view.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 5478398dfb6..84e946ecf70 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -6,7 +6,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Action, IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { raceTimeout, RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isErrorWithActions } from 'vs/base/common/errorMessage'; import * as errors from 'vs/base/common/errors'; @@ -24,7 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -34,22 +34,21 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { EditorsOrder } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_HAS_DEBUGGED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, DEBUG_SCHEME, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; +import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; @@ -58,6 +57,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -1081,8 +1081,8 @@ export class DebugService implements IDebugService { await this.sendFunctionBreakpoints(); } - async addDataBreakpoint(description: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise { - this.model.addDataBreakpoint({ description, dataId, canPersist, accessTypes, accessType, mode }); + async addDataBreakpoint(opts: IDataBreakpointOptions): Promise { + this.model.addDataBreakpoint(opts); this.debugStorage.storeBreakpoints(this.model); await this.sendDataBreakpoints(); this.debugStorage.storeBreakpoints(this.model); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 861135c6f6a..0d56d661268 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -29,7 +29,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -41,6 +41,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isDefined } from 'vs/base/common/types'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -461,7 +462,7 @@ export class DebugSession implements IDebugSession, IDisposable { breakpoints: breakpointsToSend.map(bp => bp.toDAP()), sourceModified }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < breakpointsToSend.length; i++) { data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]); @@ -478,7 +479,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts.map(bp => bp.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < fbpts.length; i++) { data.set(fbpts[i].getId(), response.body.breakpoints[i]); @@ -506,7 +507,7 @@ export class DebugSession implements IDebugSession, IDisposable { } : { filters: exbpts.map(exb => exb.filter) }; const response = await this.raw.setExceptionBreakpoints(args); - if (response && response.body && response.body.breakpoints) { + if (response?.body && response.body.breakpoints) { const data = new Map(); for (let i = 0; i < exbpts.length; i++) { data.set(exbpts[i].getId(), response.body.breakpoints[i]); @@ -517,7 +518,19 @@ export class DebugSession implements IDebugSession, IDisposable { } } - async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + if (this.raw?.capabilities.supportsDataBreakpointBytes === false) { + throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes")); + } + + return this._dataBreakpointInfo({ name: address, bytes, asAddress: true }); + } + + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + return this._dataBreakpointInfo({ name, variablesReference }); + } + + private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } @@ -525,7 +538,7 @@ export class DebugSession implements IDebugSession, IDisposable { throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); } - const response = await this.raw.dataBreakpointInfo({ name, variablesReference }); + const response = await this.raw.dataBreakpointInfo(args); return response?.body; } @@ -535,11 +548,24 @@ export class DebugSession implements IDebugSession, IDisposable { } if (this.raw.readyForBreakpoints) { - const response = await this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints.map(bp => bp.toDAP()) }); - if (response && response.body) { + const converted = await Promise.all(dataBreakpoints.map(async bp => { + try { + const dap = await bp.toDAP(this); + return { dap, bp }; + } catch (e) { + return { bp, message: e.message }; + } + })); + const response = await this.raw.setDataBreakpoints({ breakpoints: converted.map(d => d.dap).filter(isDefined) }); + if (response?.body) { const data = new Map(); - for (let i = 0; i < dataBreakpoints.length; i++) { - data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]); + let i = 0; + for (const dap of converted) { + if (!dap.dap) { + data.set(dap.bp.getId(), dap.message); + } else if (i < response.body.breakpoints.length) { + data.set(dap.bp.getId(), response.body.breakpoints[i++]); + } } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } @@ -553,7 +579,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < instructionBreakpoints.length; i++) { data.set(instructionBreakpoints[i].getId(), response.body.breakpoints[i]); @@ -790,7 +816,7 @@ export class DebugSession implements IDebugSession, IDisposable { } const response = await this.raw.loadedSources({}); - if (response && response.body && response.body.sources) { + if (response?.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; @@ -959,7 +985,7 @@ export class DebugSession implements IDebugSession, IDisposable { private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise { if (this.raw) { const response = await this.raw.threads(); - if (response && response.body && response.body.threads) { + if (response?.body && response.body.threads) { this.model.rawUpdate({ sessionId: this.getId(), threads: response.body.threads, diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 0b9fb05501d..edf29f96d3a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/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/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index e092df64537..92f139a3721 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { $, Dimension, addStandardDisposableListener, append, getWindowById } from 'vs/base/browser/dom'; +import { $, Dimension, addStandardDisposableListener, append } from 'vs/base/browser/dom'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { binarySearch2 } from 'vs/base/common/arrays'; @@ -42,6 +42,7 @@ import { InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugMo import { getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { isUri, sourcesEqual } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; interface IDisassembledInstructionEntry { allowBreakpoint: boolean; @@ -92,6 +93,7 @@ export class DisassemblyView extends EditorPane { private readonly _referenceToMemoryAddress = new Map(); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -99,7 +101,7 @@ export class DisassemblyView extends EditorPane { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDebugService private readonly _debugService: IDebugService, ) { - super(DISASSEMBLY_VIEW_ID, telemetryService, themeService, storageService); + super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService); this._disassembledInstructions = undefined; this._onDidChangeStackFrame = this._register(new Emitter({ leakWarningThreshold: 1000 })); @@ -133,8 +135,7 @@ export class DisassemblyView extends EditorPane { } private createFontInfo() { - const window = getWindowById(this.group?.windowId, true).window; - return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(window).value); + return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value); } get currentInstructionAddresses() { diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index ffcda601ff9..21f7ed8ba47 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -25,7 +25,7 @@ import 'vs/css!./media/repl'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 1ba443a95f1..c227a6d18f5 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -28,6 +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/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = dom.$; @@ -82,7 +84,7 @@ export class ReplEvaluationInputsRenderer implements ITreeRenderer element.sourceData; } @@ -297,7 +299,7 @@ export class ReplRawObjectsRenderer 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/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 86d0e94b826..9355d683c3c 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,7 +24,7 @@ import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDataBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -62,6 +62,7 @@ export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); +export const CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES = new RawContextKey('breakpointItemBytes', undefined, { type: 'boolean', description: nls.localize('breakpointItemIsDataBytes', "Whether the breakpoint item is a data breakpoint on a byte range.") }); export const CONTEXT_BREAKPOINT_HAS_MODES = new RawContextKey('breakpointHasModes', false, { type: 'boolean', description: nls.localize('breakpointHasModes', "Whether the breakpoint has multiple modes it can switch to.") }); export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false, { type: 'boolean', description: nls.localize('breakpointSupportsCondition', "True when the focused breakpoint supports conditions.") }); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false, { type: 'boolean', description: nls.localize('loadedScriptsSupported', "True when the focused sessions supports the LOADED SCRIPTS view") }); @@ -78,6 +79,7 @@ export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggers export const CONTEXT_DEBUG_EXTENSION_AVAILABLE = new RawContextKey('debugExtensionAvailable', true, { type: 'boolean', description: nls.localize('debugExtensionsAvailable', "True when there is at least one debug extension installed and enabled.") }); export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey('debugProtocolVariableMenuContext', undefined, { type: 'string', description: nls.localize('debugProtocolVariableMenuContext', "Represents the context the debug adapter sets on the focused variable in the VARIABLES view.") }); export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugSetVariableSupported', false, { type: 'boolean', description: nls.localize('debugSetVariableSupported', "True when the focused session supports 'setVariable' request.") }); +export const CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED = new RawContextKey('debugSetDataBreakpointAddressSupported', false, { type: 'boolean', description: nls.localize('debugSetDataBreakpointAddressSupported', "True when the focused session supports 'getBreakpointInfo' request on an address.") }); export const CONTEXT_SET_EXPRESSION_SUPPORTED = new RawContextKey('debugSetExpressionSupported', false, { type: 'boolean', description: nls.localize('debugSetExpressionSupported', "True when the focused session supports 'setExpression' request.") }); export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueChangesSupported', "True when the focused session supports to break when value changes.") }); export const CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED = new RawContextKey('breakWhenValueIsAccessedSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsAccessedSupported', "True when the focused breakpoint supports to break when value is accessed.") }); @@ -88,6 +90,7 @@ export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey export const CONTEXT_VARIABLE_IS_READONLY = new RawContextKey('variableIsReadonly', false, { type: 'boolean', description: nls.localize('variableIsReadonly', "True when the focused variable is read-only.") }); export const CONTEXT_VARIABLE_VALUE = new RawContextKey('variableValue', false, { type: 'string', description: nls.localize('variableValue', "Value of the variable, present for debug visualization clauses.") }); export const CONTEXT_VARIABLE_TYPE = new RawContextKey('variableType', false, { type: 'string', description: nls.localize('variableType', "Type of the variable, present for debug visualization clauses.") }); +export const CONTEXT_VARIABLE_INTERFACES = new RawContextKey('variableInterfaces', false, { type: 'array', description: nls.localize('variableInterfaces', "Any interfaces or contracts that the variable satisfies, present for debug visualization clauses.") }); export const CONTEXT_VARIABLE_NAME = new RawContextKey('variableName', false, { type: 'string', description: nls.localize('variableName', "Name of the variable, present for debug visualization clauses.") }); export const CONTEXT_VARIABLE_LANGUAGE = new RawContextKey('variableLanguage', false, { type: 'string', description: nls.localize('variableLanguage', "Language of the variable source, present for debug visualization clauses.") }); export const CONTEXT_VARIABLE_EXTENSIONID = new RawContextKey('variableExtensionId', false, { type: 'string', description: nls.localize('variableExtensionId', "Extension ID of the variable source, present for debug visualization clauses.") }); @@ -404,6 +407,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 +611,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 +738,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 +1163,7 @@ export interface IDebugService { /** * Adds a new data breakpoint. */ - addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise; + addDataBreakpoint(opts: IDataBreakpointOptions): Promise; /** * Updates an already existing data breakpoint. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 8098d1ce57b..b12e9b726ff 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -1150,15 +1150,18 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export interface IDataBreakpointOptions extends IBaseBreakpointOptions { description: string; - dataId: string; + src: DataBreakpointSource; canPersist: boolean; + initialSessionData?: { session: IDebugSession; dataId: string }; accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; accessType: DebugProtocol.DataBreakpointAccessType; } export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { + private readonly sessionDataIdForAddr = new WeakMap(); + public readonly description: string; - public readonly dataId: string; + public readonly src: DataBreakpointSource; public readonly canPersist: boolean; public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; public readonly accessType: DebugProtocol.DataBreakpointAccessType; @@ -1169,15 +1172,36 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { ) { super(id, opts); this.description = opts.description; - this.dataId = opts.dataId; + if ('dataId' in opts) { // back compat with old saved variables in 1.87 + opts.src = { type: DataBreakpointSetType.Variable, dataId: opts.dataId as string }; + } + this.src = opts.src; this.canPersist = opts.canPersist; this.accessTypes = opts.accessTypes; this.accessType = opts.accessType; + if (opts.initialSessionData) { + this.sessionDataIdForAddr.set(opts.initialSessionData.session, opts.initialSessionData.dataId); + } } - toDAP(): DebugProtocol.DataBreakpoint { + async toDAP(session: IDebugSession): Promise { + let dataId: string; + if (this.src.type === DataBreakpointSetType.Variable) { + dataId = this.src.dataId; + } else { + let sessionDataId = this.sessionDataIdForAddr.get(session); + if (!sessionDataId) { + sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; + if (!sessionDataId) { + return undefined; + } + this.sessionDataIdForAddr.set(session, sessionDataId); + } + dataId = sessionDataId; + } + return { - dataId: this.dataId, + dataId, accessType: this.accessType, condition: this.condition, hitCondition: this.hitCondition, @@ -1188,7 +1212,7 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { return { ...super.toJSON(), description: this.description, - dataId: this.dataId, + src: this.src, accessTypes: this.accessTypes, accessType: this.accessType, canPersist: this.canPersist, diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index b00a4fd466a..50eacfd65e2 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -813,11 +813,22 @@ declare module DebugProtocol { /** Reference to the variable container if the data breakpoint is requested for a child of the container. The `variablesReference` must have been obtained in the current suspended state. See 'Lifetime of Object References' in the Overview section for details. */ variablesReference?: number; /** The name of the variable's child to obtain data breakpoint information for. - If `variablesReference` isn't specified, this can be an expression. + If `variablesReference` isn't specified, this can be an expression, or an address if `asAddress` is also true. */ name: string; /** When `name` is an expression, evaluate it in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. When `variablesReference` is specified, this property has no effect. */ frameId?: number; + /** If specified, a debug adapter should return information for the range of memory extending `bytes` number of bytes from the address or variable specified by `name`. Breakpoints set using the resulting data ID should pause on data access anywhere within that range. + + Clients may set this property only if the `supportsDataBreakpointBytes` capability is true. + */ + bytes?: number; + /** If `true`, the `name` is a memory address and the debugger should interpret it as a decimal value, or hex value if it is prefixed with `0x`. + + Clients may set this property only if the `supportsDataBreakpointBytes` + capability is true. + */ + asAddress?: boolean; /** The mode of the desired breakpoint. If defined, this must be one of the `breakpointModes` the debug adapter advertised in its `Capabilities`. */ mode?: string; } @@ -1680,42 +1691,6 @@ declare module DebugProtocol { }; } - /** DataAddressBreakpointInfo request; value of command field is 'DataAddressBreakpointInfo'. - Obtains information on a possible data breakpoint that could be set on a memory address or memory address range. - - Clients should only call this request if the corresponding capability `supportsDataAddressInfo` is true. - */ - interface DataAddressBreakpointInfoRequest extends Request { - // command: 'DataAddressBreakpointInfo'; - arguments: DataAddressBreakpointInfoArguments; - } - - /** Arguments for `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoArguments { - /** The address of the data for which to obtain breakpoint information. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - address?: string; - /** If passed, requests breakpoint information for an exclusive byte range rather than a single address. The range extends the given number of `bytes` from the start `address`. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - bytes?: string; - } - - /** Response to `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoResponse extends Response { - body: { - /** An identifier for the data on which a data breakpoint can be registered with the `setDataBreakpoints` request or null if no data breakpoint is available. If a `variablesReference` or `frameId` is passed, the `dataId` is valid in the current suspended state, otherwise it's valid indefinitely. See 'Lifetime of Object References' in the Overview section for details. Breakpoints set using the `dataId` in the `setDataBreakpoints` request may outlive the lifetime of the associated `dataId`. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Attribute lists the available access types for a potential data breakpoint. A UI client could surface this information. */ - accessTypes?: DataBreakpointAccessType[]; - /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - }; - } - /** Information about the capabilities of a debug adapter. */ interface Capabilities { /** The debug adapter supports the `configurationDone` request. */ @@ -1788,8 +1763,6 @@ declare module DebugProtocol { supportsBreakpointLocationsRequest?: boolean; /** The debug adapter supports the `clipboard` context value in the `evaluate` request. */ supportsClipboardContext?: boolean; - /** The debug adapter supports the `dataAddressBreakpointInfo` request. */ - supportsDataAddressInfo?: boolean; /** The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. */ supportsSteppingGranularity?: boolean; /** The debug adapter supports adding breakpoints based on instruction references. */ @@ -1798,6 +1771,8 @@ declare module DebugProtocol { supportsExceptionFilterOptions?: boolean; /** The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). */ supportsSingleThreadExecutionRequests?: boolean; + /** The debug adapter supports the `asAddress` and `bytes` fields in the `dataBreakpointInfo` request. */ + supportsDataBreakpointBytes?: boolean; /** Modes of breakpoints supported by the debug adapter, such as 'hardware' or 'software'. If present, the client may allow the user to select a mode and include it in its `setBreakpoints` request. Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 4b0959a97a8..7221f390771 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { @@ -34,6 +34,7 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private setDataBreakpointAtByteSupported!: IContextKey; private setExpressionSupported!: IContextKey; private multiSessionDebug!: IContextKey; private terminateDebuggeeSupported!: IContextKey; @@ -52,6 +53,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.setDataBreakpointAtByteSupported = CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED.bindTo(contextKeyService); this.setExpressionSupported = CONTEXT_SET_EXPRESSION_SUPPORTED.bindTo(contextKeyService); this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); this.terminateDebuggeeSupported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); @@ -88,15 +90,16 @@ export class ViewModel implements IViewModel { this._focusedSession = session; this.contextKeyService.bufferChangeEvents(() => { - this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false); - this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false); - this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); - this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false); - this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); - this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); - this.setExpressionSupported.set(session ? !!session.capabilities.supportsSetExpression : false); - this.terminateDebuggeeSupported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); - this.suspendDebuggeeSupported.set(session ? !!session.capabilities.supportSuspendDebuggee : false); + this.loadedScriptsSupportedContextKey.set(!!session?.capabilities.supportsLoadedSourcesRequest); + this.stepBackSupportedContextKey.set(!!session?.capabilities.supportsStepBack); + this.restartFrameSupportedContextKey.set(!!session?.capabilities.supportsRestartFrame); + this.stepIntoTargetsSupported.set(!!session?.capabilities.supportsStepInTargetsRequest); + this.jumpToCursorSupported.set(!!session?.capabilities.supportsGotoTargetsRequest); + this.setVariableSupported.set(!!session?.capabilities.supportsSetVariable); + this.setDataBreakpointAtByteSupported.set(!!session?.capabilities.supportsDataBreakpointBytes); + this.setExpressionSupported.set(!!session?.capabilities.supportsSetExpression); + this.terminateDebuggeeSupported.set(!!session?.capabilities.supportTerminateDebuggee); + this.suspendDebuggeeSupported.set(!!session?.capabilities.supportSuspendDebuggee); this.disassembleRequestSupported.set(!!session?.capabilities.supportsDisassembleRequest); this.focusedStackFrameHasInstructionPointerReference.set(!!stackFrame?.instructionPointerReference); const attach = !!session && isSessionAttach(session); diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index c3f3cd92928..84c3d7947d4 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/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/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 43c4f7d0b11..173efc27ee0 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/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/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index b85e544f9bb..61599c36ce9 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -19,7 +19,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { getBreakpointMessageAndIcon, getExpandedBodySize } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DataBreakpointSetType, IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; @@ -313,13 +313,13 @@ suite('Debug - Breakpoints', () => { let eventCount = 0; disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); - model.addDataBreakpoint({ description: 'label', dataId: 'id', canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); - model.addDataBreakpoint({ description: 'second', dataId: 'secondId', canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); + model.addDataBreakpoint({ description: 'label', src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); + model.addDataBreakpoint({ description: 'second', src: { type: DataBreakpointSetType.Variable, dataId: 'secondId' }, canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); model.updateDataBreakpoint('1', { condition: 'aCondition' }); model.updateDataBreakpoint('2', { hitCondition: '10' }); const dataBreakpoints = model.getDataBreakpoints(); assert.strictEqual(dataBreakpoints[0].canPersist, true); - assert.strictEqual(dataBreakpoints[0].dataId, 'id'); + assert.deepStrictEqual(dataBreakpoints[0].src, { type: DataBreakpointSetType.Variable, dataId: 'id' }); assert.strictEqual(dataBreakpoints[0].accessType, 'read'); assert.strictEqual(dataBreakpoints[0].condition, 'aCondition'); assert.strictEqual(dataBreakpoints[1].canPersist, false); @@ -374,7 +374,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Disabled Logpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log-disabled'); - model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', dataId: 'id' }); + model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', src: { type: DataBreakpointSetType.Variable, dataId: 'id' } }); const dataBreakpoints = model.getDataBreakpoints(); result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0], ls, model); assert.strictEqual(result.message, 'Data Breakpoint'); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 617f46d449f..464a4794def 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -13,7 +13,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -114,7 +114,7 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { + addDataBreakpoint(): Promise { throw new Error('Method not implemented.'); } @@ -223,6 +223,10 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + throw new Error('Method not implemented.'); + } + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined } | undefined> { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 5baf0bb721d..000198bfdd4 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/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/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 6ba3511f0ee..f612c22c3e3 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -346,8 +346,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + if (!signedInForProvider || this.authenticationService.getProvider(authenticationProvider.id).supportsMultipleAccounts) { + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider }); } } @@ -370,7 +370,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const session of sessions) { const item = { label: session.account.label, - description: this.authenticationService.getLabel(provider.id), + description: this.authenticationService.getProvider(provider.id).label, session: { ...session, providerId: provider.id } }; accounts.set(item.session.account.id, item); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts index 5793dde0157..ce4af2c979b 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts +++ b/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/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index 4ad6b2fe900..c6be96b073d 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -37,7 +37,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService, IPrompt } from 'vs/platform/dialogs/common/dialogs'; import { IEditorService, ISaveAllEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -213,7 +213,7 @@ suite('Edit session sync', () => { // Create root folder await fileService.createFolder(folderUri); - await editSessionsContribution.storeEditSession(true, new CancellationTokenSource().token); + await editSessionsContribution.storeEditSession(true, CancellationToken.None); // Verify that we did not attempt to write the edit session assert.equal(writeStub.called, false); diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 991a3df035c..7c7fbac3a95 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -5,6 +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/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'; @@ -36,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'; @@ -75,6 +78,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { private _updateSoon: RunOnceScheduler; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -89,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; @@ -364,13 +368,15 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } else { title = nls.localize('extensionActivating', "Extension is activating..."); } - data.activationTime.title = title; + data.elementDisposables.push(setupCustomHover(getDefaultHoverDelegate('mouse'), data.activationTime, title)); clearNode(data.msgContainer); if (this._getUnresponsiveProfile(element.description.identifier)) { const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`)); - el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); + const extensionHostFreezTitle = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); + data.elementDisposables.push(setupCustomHover(getDefaultHoverDelegate('mouse'), el, extensionHostFreezTitle)); + data.msgContainer.appendChild(el); } @@ -416,7 +422,9 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } if (accessData?.current) { const element = $('span', undefined, nls.localize('requests count', "{0} Requests: {1} (Session)", feature.label, accessData.current.count)); - element.title = nls.localize('requests count title', "Last request was {0}. Overall Requests: {1}", fromNow(accessData.current.lastAccessed, true, true), accessData.totalCount); + const title = nls.localize('requests count title', "Last request was {0}. Overall Requests: {1}", fromNow(accessData.current.lastAccessed, true, true), accessData.totalCount); + data.elementDisposables.push(setupCustomHover(getDefaultHoverDelegate('mouse'), element, title)); + data.msgContainer.appendChild(element); } } diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index 775e2254338..28ae3c3e3ed 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts b/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts index 2345dedabd3..6b85011360d 100644 --- a/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 6096e57e82a..9e75f3fb4a9 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 21986a4b709..26de19cb281 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -5,6 +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/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'; @@ -42,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'; @@ -59,7 +62,7 @@ import { InstallDropdownAction, InstallingLabelAction, LocalInstallAction, MigrateDeprecatedExtensionAction, - ReloadAction, + ExtensionRuntimeStateAction, RemoteInstallAction, SetColorThemeAction, SetFileIconThemeAction, @@ -76,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 { @@ -152,6 +160,7 @@ interface IExtensionEditorTemplate { builtin: HTMLElement; publisher: HTMLElement; publisherDisplayName: HTMLElement; + resource: HTMLElement; installCount: HTMLElement; rating: HTMLElement; description: HTMLElement; @@ -188,7 +197,8 @@ class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget { private readonly element: HTMLElement; constructor(container: HTMLElement) { super(); - this.element = append(container, $('code.version', { title: localize('extension version', "Extension Version") })); + this.element = append(container, $('code.version')); + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version"))); this.render(); } render(): void { @@ -225,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, @@ -240,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; @@ -268,25 +283,33 @@ export class ExtensionEditor extends EditorPane { const details = append(header, $('.details')); const title = append(details, $('.title')); - const name = append(title, $('span.name.clickable', { title: localize('name', "Extension name"), role: 'heading', tabIndex: 0 })); + const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 })); + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name"))); const versionWidget = new VersionWidget(title); - const preview = append(title, $('span.preview', { title: localize('preview', "Preview") })); + const preview = append(title, $('span.preview')); + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview"))); preview.textContent = localize('preview', "Preview"); const builtin = append(title, $('span.builtin')); builtin.textContent = localize('builtin', "Built-in"); const subtitle = append(details, $('.subtitle')); - const publisher = append(append(subtitle, $('.subtitle-entry')), $('.publisher.clickable', { title: localize('publisher', "Publisher"), tabIndex: 0 })); + const publisher = append(append(subtitle, $('.subtitle-entry')), $('.publisher.clickable', { tabIndex: 0 })); + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), publisher, localize('publisher', "Publisher"))); publisher.setAttribute('role', 'button'); const publisherDisplayName = append(publisher, $('.publisher-name')); const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $('.verified-publisher')), false); - const installCount = append(append(subtitle, $('.subtitle-entry')), $('span.install', { title: localize('install count', "Install count"), tabIndex: 0 })); + 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); - const rating = append(append(subtitle, $('.subtitle-entry')), $('span.rating.clickable', { title: localize('rating', "Rating"), tabIndex: 0 })); + const rating = append(append(subtitle, $('.subtitle-entry')), $('span.rating.clickable', { tabIndex: 0 })); + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), rating, localize('rating', "Rating"))); rating.setAttribute('role', 'link'); // #132645 const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, rating, false); @@ -305,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'])]]), @@ -336,15 +359,15 @@ export class ExtensionEditor extends EditorPane { const actionsAndStatusContainer = append(details, $('.actions-status-container')); const extensionActionBar = this._register(new ActionBar(actionsAndStatusContainer, { - actionViewItemProvider: (action: IAction) => { + actionViewItemProvider: (action: IAction, options) => { if (action instanceof ExtensionDropDownAction) { - return action.createActionViewItem(); + return action.createActionViewItem(options); } if (action instanceof ActionWithDropDownAction) { - return new ExtensionActionWithDropdownActionViewItem(action, { icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); + return new ExtensionActionWithDropdownActionViewItem(action, { ...options, icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); } if (action instanceof ToggleAutoUpdateForExtensionAction) { - return new CheckboxActionViewItem(undefined, action, { icon: true, label: true, checkboxStyles: defaultCheckboxStyles }); + return new CheckboxActionViewItem(undefined, action, { ...options, icon: true, label: true, checkboxStyles: defaultCheckboxStyles }); } return undefined; }, @@ -409,6 +432,7 @@ export class ExtensionEditor extends EditorPane { preview, publisher, publisherDisplayName, + resource, rating, actionsAndStatusContainer, extensionActionBar, @@ -463,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; } @@ -511,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); @@ -666,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))); @@ -914,7 +963,9 @@ export class ExtensionEditor extends EditorPane { append(extensionResourcesContainer, $('.additional-details-title', undefined, localize('resources', "Resources"))); const resourcesElement = append(extensionResourcesContainer, $('.resources')); for (const [label, uri] of resources) { - this.transientDisposables.add(onClick(append(resourcesElement, $('a.resource', { title: uri.toString(), tabindex: '0' }, label)), () => this.openerService.open(uri))); + const resource = append(resourcesElement, $('a.resource', { tabindex: '0' }, label)); + this.transientDisposables.add(onClick(resource, () => this.openerService.open(uri))); + this.transientDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), resource, uri.toString())); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts index 8bd7b3cd177..a679ef876b4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 18274088c5b..96c6d793468 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/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(); } @@ -179,14 +185,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; } @@ -203,7 +211,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; @@ -224,7 +232,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; } @@ -233,7 +241,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) { @@ -414,15 +422,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/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index fc28afa9003..bc811fe8ccf 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index a6806b20159..b266d936a12 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/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'; @@ -150,9 +151,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; } } @@ -162,8 +163,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) }; } @@ -177,11 +178,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; } @@ -194,43 +192,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 { @@ -254,10 +259,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; } @@ -272,8 +280,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/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index c2747ac3285..d7783e50528 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,8 +8,8 @@ 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 { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } 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 { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -79,6 +79,7 @@ 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'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -121,13 +122,9 @@ Registry.as(ViewContainerExtensions.ViewContainersRegis alwaysUseContainerInfo: true, }, ViewContainerLocation.Sidebar); - Registry.as(ConfigurationExtensions.Configuration) .registerConfiguration({ - id: 'extensions', - order: 30, - title: localize('extensionsConfigurationTitle', "Extensions"), - type: 'object', + ...extensionsConfigurationNodeBase, properties: { 'extensions.autoUpdate': { enum: [true, 'onlyEnabledExtensions', 'onlySelectedExtensions', false,], @@ -255,6 +252,11 @@ 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 } } }); @@ -318,6 +320,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."), @@ -327,31 +338,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); @@ -1507,7 +1531,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) => { @@ -1524,7 +1548,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) => { @@ -1565,7 +1589,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) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 1fc58a9e0f5..ac83a648938 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { disposeIfDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -73,6 +73,7 @@ import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConst import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IUpdateService } from 'vs/platform/update/common/update'; export class PromptExtensionInstallFailureAction extends Action { @@ -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"); @@ -1021,8 +1026,8 @@ export abstract class ExtensionDropDownAction extends ExtensionAction { } private _actionViewItem: DropDownMenuActionViewItem | null = null; - createActionViewItem(): DropDownMenuActionViewItem { - this._actionViewItem = this.instantiationService.createInstance(DropDownMenuActionViewItem, this); + createActionViewItem(options: IActionViewItemOptions): DropDownMenuActionViewItem { + this._actionViewItem = this.instantiationService.createInstance(DropDownMenuActionViewItem, this, options); return this._actionViewItem; } @@ -1034,10 +1039,12 @@ export abstract class ExtensionDropDownAction extends ExtensionAction { export class DropDownMenuActionViewItem extends ActionViewItem { - constructor(action: ExtensionDropDownAction, + constructor( + action: ExtensionDropDownAction, + options: IActionViewItemOptions, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { - super(null, action, { icon: true, label: true }); + super(null, action, { ...options, icon: true, label: true }); } public showMenu(menuActionGroups: IAction[][], disposeActionsOnHide: boolean): void { @@ -1077,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]); @@ -1384,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(); @@ -1411,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); @@ -1442,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); @@ -1476,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); @@ -1509,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); @@ -1551,18 +1562,22 @@ 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, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { - super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); + super('extensions.runtimeState', '', ExtensionRuntimeStateAction.DisabledClass, false); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); this.update(); } @@ -1570,27 +1585,73 @@ 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) { + return; + } + + type ExtensionRuntimeStateActionClassification = { + owner: 'sandy081'; + comment: 'Extension runtime state action event'; + action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Executed action' }; + }; + type ExtensionRuntimeStateActionEvent = { + action: string; + }; + this.telemetryService.publicLog2('extensions:runtimestate:action', { + action: runtimeState.action + }); + + 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(); + } + } } @@ -2499,7 +2560,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/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 1bf769f4c66..2466a15d0e9 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/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'; @@ -26,6 +26,7 @@ import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { verifiedPublisherIcon as verifiedPublisherThemeIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; const EXTENSION_LIST_ELEMENT_HEIGHT = 72; @@ -98,12 +99,12 @@ export class Renderer implements IPagedRenderer { const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $(`.verified-publisher`)), true); const publisherDisplayName = append(publisher, $('.publisher-name.ellipsis')); const actionbar = new ActionBar(footer, { - actionViewItemProvider: (action: IAction) => { + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof ActionWithDropDownAction) { - return new ExtensionActionWithDropdownActionViewItem(action, { icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); + return new ExtensionActionWithDropdownActionViewItem(action, { ...options, icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); } if (action instanceof ExtensionDropDownAction) { - return action.createActionViewItem(); + return action.createActionViewItem(options); } return undefined; }, @@ -116,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), @@ -222,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/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 38c0e666af1..afa823bb849 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index adcd04b081f..09f88bf6bf8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index a4a887e596a..50c5e355972 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/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 { 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,6 +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/hoverDelegateFactory'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -124,6 +126,8 @@ export class InstallCountWidget extends ExtensionWidget { export class RatingsWidget extends ExtensionWidget { + private readonly containerHover: ICustomHover; + constructor( private container: HTMLElement, private small: boolean @@ -135,12 +139,13 @@ export class RatingsWidget extends ExtensionWidget { container.classList.add('small'); } + this.containerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), container, '')); + this.render(); } render(): void { this.container.innerText = ''; - this.container.title = ''; if (!this.extension) { return; @@ -159,7 +164,7 @@ export class RatingsWidget extends ExtensionWidget { } const rating = Math.round(this.extension.rating * 2) / 2; - this.container.title = localize('ratedLabel', "Average rating: {0} out of 5", rating); + this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating)); if (this.small) { append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon))); @@ -186,6 +191,7 @@ export class RatingsWidget extends ExtensionWidget { export class VerifiedPublisherWidget extends ExtensionWidget { private disposables = this._register(new DisposableStore()); + private readonly containerHover: ICustomHover; constructor( private container: HTMLElement, @@ -193,6 +199,7 @@ export class VerifiedPublisherWidget extends ExtensionWidget { @IOpenerService private readonly openerService: IOpenerService, ) { super(); + this.containerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), container, '')); this.render(); } @@ -203,13 +210,21 @@ 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)); if (!this.small) { verifiedPublisher.tabIndex = 0; - verifiedPublisher.title = `Verified Domain: ${this.extension.publisherDomain.link}`; + this.containerHover.update(`Verified Domain: ${this.extension.publisherDomain.link}`); verifiedPublisher.setAttribute('role', 'link'); append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority)); @@ -239,7 +254,8 @@ export class SponsorWidget extends ExtensionWidget { return; } - const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0, title: this.extension?.publisherSponsorLink })); + const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0 })); + this.disposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? '')); sponsor.setAttribute('role', 'link'); // #132645 const sponsorIconElement = renderIcon(sponsorIcon); const label = $('span', undefined, localize('sponsor', "Sponsor")); @@ -367,6 +383,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { class RemoteBadge extends Disposable { readonly element: HTMLElement; + readonly elementHover: ICustomHover; constructor( private readonly tooltip: boolean, @@ -376,6 +393,7 @@ class RemoteBadge extends Disposable { ) { super(); this.element = $('div.extension-badge.extension-remote-badge'); + this.elementHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.element, '')); this.render(); } @@ -397,7 +415,7 @@ class RemoteBadge extends Disposable { if (this.tooltip) { const updateTitle = () => { if (this.element && this.extensionManagementServerService.remoteExtensionManagementServer) { - this.element.title = localize('remote extension title', "Extension in {0}", this.extensionManagementServerService.remoteExtensionManagementServer.label); + this.elementHover.update(localize('remote extension title', "Extension in {0}", this.extensionManagementServerService.remoteExtensionManagementServer.label)); } }; this._register(this.labelService.onDidChangeFormatters(() => updateTitle())); @@ -435,6 +453,8 @@ export class ExtensionPackCountWidget extends ExtensionWidget { export class SyncIgnoredWidget extends ExtensionWidget { + private readonly disposables = this._register(new DisposableStore()); + constructor( private readonly container: HTMLElement, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -448,11 +468,12 @@ export class SyncIgnoredWidget extends ExtensionWidget { } render(): void { + this.disposables.clear(); this.container.innerText = ''; if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) { const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon))); - element.title = localize('syncingore.label', "This extension is ignored during sync."); + this.disposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync."))); element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon)); } } @@ -517,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(); } @@ -583,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`); @@ -604,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`); @@ -644,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/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index fa11076ab65..89ea0e56a44 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/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,153 @@ 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 && 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) { + 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; + } } - return this.installFromGallery(extension, extension.gallery, installOptions); - }, progressLocation); + } + + 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) || !installOptions.justification.action + ? 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; + } + } + 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 +2087,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 +2153,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 +2176,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 +2194,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 +2214,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/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index c043223dc06..74ce489d01d 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index a40eed0f23f..3ad131168e5 100644 --- a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts index 9258305b84a..ac493c927fe 100644 --- a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 136ce1af0f7..985b5511c05 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/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/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 44f8b720581..59a07a33c6f 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/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/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts index 43d9c8a2dd8..f3ccbdd5a8b 100644 --- a/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts index 8688c14b824..bb72b0236d1 100644 --- a/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index e2e34e8f8fc..81b3d737aa5 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/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/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 3e5d85b7bf2..737e37568bc 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/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/src/vs/workbench/contrib/extensions/common/extensionsInput.ts b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts index c8d1a1b1861..8215c4ef18d 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts +++ b/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/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index a1304365806..ff915c04865 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/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/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 11dc035fb5b..14cb766b00d 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -30,6 +30,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { IExtensionFeaturesManagementService } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -65,6 +66,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { private _profileSessionState: IContextKey; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -80,7 +82,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); + super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts index 4186a04ec35..8c96cf7a48e 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts +++ b/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/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 1e9e11baa19..0e382e8c941 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -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/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 0762678249d..ca20f73669d 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -56,6 +56,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/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 4a06d92c0e8..7ff4ea1b27a 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -48,6 +48,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/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index ab080153b5e..e61f94d31bd 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -51,6 +51,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/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index ad722bacc27..1b6b279b308 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -15,7 +15,7 @@ import { EditorResolution, IEditorOptions } from 'vs/platform/editor/common/edit import { IEditorResolverService, ResolvedStatus, ResolvedEditor } from 'vs/workbench/services/editor/common/editorResolverService'; import { isEditorInputWithOptions } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; /** * An implementation of editor for binary files that cannot be displayed. @@ -25,14 +25,15 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { static readonly ID = BINARY_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @IStorageService storageService: IStorageService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + @IStorageService storageService: IStorageService ) { super( BinaryFileEditor.ID, + group, { openInternal: (input, options) => this.openInternal(input, options) }, @@ -43,7 +44,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } private async openInternal(input: EditorInput, options: IEditorOptions | undefined): Promise { - if (input instanceof FileEditorInput && this.group?.activeEditor) { + if (input instanceof FileEditorInput && this.group.activeEditor) { // We operate on the active editor here to support re-opening // diff editors where `input` may just be one side of the @@ -84,7 +85,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } // Replace the active editor with the picked one - await (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + await this.group.replaceEditors([{ editor: activeEditor, replacement: resolvedEditor?.editor ?? input, options: { diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 506bf005000..994733bad77 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -25,6 +25,7 @@ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; const enum ForceOpenAs { None, @@ -98,9 +99,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IEditorService editorService: IEditorService, @IPathService private readonly pathService: IPathService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService ) { - super(resource, preferredResource, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService); + super(resource, preferredResource, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); this.model = this.textFileService.files.get(resource); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index c0f1c912060..7c01103959b 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -46,6 +46,7 @@ export class TextFileEditor extends AbstractTextCodeEditor static readonly ID = TEXT_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IFileService fileService: IFileService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -65,7 +66,7 @@ export class TextFileEditor extends AbstractTextCodeEditor @IHostService private readonly hostService: IHostService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(TextFileEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); // Clear view state for deleted files this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); @@ -192,7 +193,7 @@ export class TextFileEditor extends AbstractTextCodeEditor } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "The file is not displayed in the text editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -240,7 +241,6 @@ export class TextFileEditor extends AbstractTextCodeEditor private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { const defaultBinaryEditor = this.configurationService.getValue('workbench.editor.defaultBinaryEditor'); - const group = this.group ?? this.editorGroupService.activeGroup; const editorOptions = { ...options, @@ -259,9 +259,9 @@ export class TextFileEditor extends AbstractTextCodeEditor // and avoid enforcing binary or text on the file editor input. if (defaultBinaryEditor && defaultBinaryEditor !== '' && defaultBinaryEditor !== DEFAULT_EDITOR_ASSOCIATION.id) { - this.doOpenAsBinaryInDifferentEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInDifferentEditor(this.group, defaultBinaryEditor, input, editorOptions); } else { - this.doOpenAsBinaryInSameEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInSameEditor(this.group, defaultBinaryEditor, input, editorOptions); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 7d04ebe6d5d..066e0372c6b 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/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/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 9785e04a5b1..0a79bee3fc6 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -94,12 +94,12 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor } } - if (viewDescriptorsToRegister.length) { - viewsRegistry.registerViews(viewDescriptorsToRegister, VIEW_CONTAINER); - } if (viewDescriptorsToDeregister.length) { viewsRegistry.deregisterViews(viewDescriptorsToDeregister, VIEW_CONTAINER); } + if (viewDescriptorsToRegister.length) { + viewsRegistry.registerViews(viewDescriptorsToRegister, VIEW_CONTAINER); + } mark('code/didRegisterExplorerViews'); } diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 4d98d409450..0783c78e100 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -37,7 +37,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { coalesce } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isCancellationError } from 'vs/base/common/errors'; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 80162ffd007..cc35374830e 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/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/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 71960988d83..481d8354c7e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/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; - titleElement.title = title; + this.updateTitle(this.name); this.ariaHeaderLabel = nls.localize('explorerSection', "Explorer Section: {0}", this.name); titleElement.setAttribute('aria-label', this.ariaHeaderLabel); }; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index f4d93f91e8d..ffe551566cb 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/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/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 81b3bc3da75..eacff240266 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -26,7 +26,7 @@ import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAn import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableMap, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ResourceContextKey, MultipleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; @@ -123,7 +123,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()) { @@ -161,7 +161,6 @@ export class OpenEditorsView extends ViewPane { } }); groupDisposables.set(group.id, groupModelChangeListener); - this._register(groupDisposables.get(group.id)!); }; this.editorGroupService.groups.forEach(g => addGroupListener(g)); @@ -172,7 +171,7 @@ export class OpenEditorsView extends ViewPane { this._register(this.editorGroupService.onDidMoveGroup(() => updateWholeList())); this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.focusActiveEditor())); this._register(this.editorGroupService.onDidRemoveGroup(group => { - dispose(groupDisposables.get(group.id)); + groupDisposables.deleteAndDispose(group.id); updateWholeList(); })); } diff --git a/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts b/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts index 94849cda9f7..7e99a23b0ed 100644 --- a/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts +++ b/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/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index e1ecd72a649..f104ba762eb 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -104,7 +104,7 @@ suite('EditorAutoSave', () => { assert.strictEqual(model.isDirty(), false); - await editorPane?.group?.closeAllEditors(); + await editorPane?.group.closeAllEditors(); }); function awaitModelSaved(model: ITextFileEditorModel): Promise { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ea70b146a8c..38e11d253db 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -24,7 +24,7 @@ import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/br // --- browser registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed); -registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); +registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Eager); // EAGER because this registers an agent which we need swiftly registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors @@ -34,9 +34,8 @@ registerAction2(InlineChatActions.StartSessionAction); registerAction2(InlineChatActions.CloseAction); registerAction2(InlineChatActions.ConfigureInlineChatAction); registerAction2(InlineChatActions.UnstashSessionAction); -registerAction2(InlineChatActions.MakeRequestAction); -registerAction2(InlineChatActions.StopRequestAction); registerAction2(InlineChatActions.ReRunRequestAction); +registerAction2(InlineChatActions.ReRunRequestWithIntentDetectionAction); registerAction2(InlineChatActions.DiscardHunkAction); registerAction2(InlineChatActions.DiscardAction); registerAction2(InlineChatActions.DiscardToClipboardAction); @@ -48,15 +47,9 @@ registerAction2(InlineChatActions.MoveToPreviousHunk); registerAction2(InlineChatActions.ArrowOutUpAction); registerAction2(InlineChatActions.ArrowOutDownAction); registerAction2(InlineChatActions.FocusInlineChat); -registerAction2(InlineChatActions.PreviousFromHistory); -registerAction2(InlineChatActions.NextFromHistory); registerAction2(InlineChatActions.ViewInChatAction); -registerAction2(InlineChatActions.ExpandMessageAction); -registerAction2(InlineChatActions.ContractMessageAction); registerAction2(InlineChatActions.ToggleDiffForChange); -registerAction2(InlineChatActions.FeebackHelpfulCommand); -registerAction2(InlineChatActions.FeebackUnhelpfulCommand); registerAction2(InlineChatActions.ReportIssueForBugCommand); registerAction2(InlineChatActions.AcceptChanges); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css deleted file mode 100644 index 7d299bbc1e5..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ /dev/null @@ -1,417 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-editor .zone-widget.inline-chat-widget { - z-index: 3; -} - -.monaco-editor .zone-widget-container.inside-selection { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -.monaco-editor .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); - background: var(--vscode-inlineChat-background); -} - -/* body */ - -.monaco-editor .inline-chat .body { - display: flex; -} - -.monaco-editor .inline-chat .body .content { - display: flex; - box-sizing: border-box; - outline: 1px solid var(--vscode-inlineChatInput-border); - outline-offset: -1px; - border-radius: 2px; -} - -.monaco-editor .inline-chat .body .content.synthetic-focus { - outline: 1px solid var(--vscode-inlineChatInput-focusBorder); -} - -.monaco-editor .inline-chat .body .content .input { - display: flex; - align-items: center; - justify-content: space-between; - padding: 2px 2px 2px 6px; - background-color: var(--vscode-inlineChatInput-background); - cursor: text; -} - -.monaco-editor .inline-chat .body .content .input .monaco-editor-background { - background-color: var(--vscode-inlineChatInput-background); -} - -.monaco-editor .inline-chat .body .content .input .editor-placeholder { - position: absolute; - z-index: 1; - color: var(--vscode-inlineChatInput-placeholderForeground); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.monaco-editor .inline-chat .body .content .input .editor-placeholder.hidden { - display: none; -} - -.monaco-editor .inline-chat .body .content .input .editor-container { - vertical-align: middle; -} -.monaco-editor .inline-chat .body .toolbar { - display: flex; - flex-direction: column; - align-self: stretch; - 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 { - display: flex; - flex-direction: row; - gap: 4px; -} - -.monaco-editor .inline-chat .body > .widget-toolbar { - padding-left: 4px; -} - -/* progress bit */ - -.monaco-editor .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 { - top: 0; -} - -/* status */ - -.monaco-editor .inline-chat .status { - margin-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 { - display: none; -} - -.monaco-editor .inline-chat .status .label { - overflow: hidden; - color: var(--vscode-descriptionForeground); - font-size: 11px; - align-self: baseline; - display: inline-flex; -} - -.monaco-editor .inline-chat .status .label.hidden { - display: none; -} - -.monaco-editor .inline-chat .status .label.info { - margin-right: auto; - padding-left: 2px; -} - -.monaco-editor .inline-chat .status .label.info > .codicon { - padding: 0 5px; - font-size: 12px; - line-height: 18px; -} - -.monaco-editor .inline-chat .status .label.status { - padding-left: 10px; - padding-right: 4px; - margin-left: auto; -} - -.monaco-editor .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 { - color: var(--vscode-descriptionForeground); - padding: 5px 0px 5px 5px; -} - -.monaco-editor .inline-chat .detectedIntent.hidden { - display: none; -} - -.monaco-editor .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 { - 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; - 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 { - padding: 5px 5px; -} - -.monaco-editor .inline-chat .followUps .interactive-session-followups .monaco-button { - display: block; - color: var(--vscode-textLink-foreground); - font-size: 12px; -} - -.monaco-editor .inline-chat .followUps.hidden { - display: none; -} - -.monaco-editor .inline-chat .chatMessage { - padding: 8px 3px; -} - -.monaco-editor .inline-chat .chatMessage .chatMessageContent { - padding: 2px 2px; -} - -.monaco-editor .inline-chat .chatMessage.hidden { - display: none; -} - -.monaco-editor .inline-chat .status .label A { - color: var(--vscode-textLink-foreground); - cursor: pointer; -} - -.monaco-editor .inline-chat .status .label.error { - color: var(--vscode-errorForeground); -} - -.monaco-editor .inline-chat .status .label.warn { - color: var(--vscode-editorWarning-foreground); -} - -.monaco-editor .inline-chat .status .actions { - display: flex; -} - -.monaco-editor .inline-chat .status .actions > .monaco-button, -.monaco-editor .inline-chat .status .actions > .monaco-button-dropdown { - margin-right: 6px; -} - -.monaco-editor .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 { - display: flex; -} - -.monaco-editor .inline-chat .status .actions > .monaco-button.codicon::before { - align-self: center; -} - -.monaco-editor .inline-chat .status .actions .monaco-text-button { - padding: 2px 4px; - white-space: nowrap; -} - -.monaco-editor .inline-chat .status .monaco-toolbar .action-item { - padding: 0 2px; -} - -/* TODO@jrieken not needed? */ -.monaco-editor .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) { - background-color: var(--vscode-button-hoverBackground); -} - -/* preview */ - -.monaco-editor .inline-chat .preview { - display: none; -} - -.monaco-editor .inline-chat .previewDiff, -.monaco-editor .inline-chat .previewCreate { - display: inherit; - border: 1px solid var(--vscode-inlineChat-border); - border-radius: 2px; - margin: 6px 0px; -} - -.monaco-editor .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 { - display: none; -} - -.monaco-editor .inline-chat-toolbar { - display: flex; -} - -.monaco-editor .inline-chat-toolbar > .monaco-button{ - margin-right: 6px; -} - -.monaco-editor .inline-chat-toolbar .action-label.checked { - color: var(--vscode-inputOption-activeForeground); - background-color: var(--vscode-inputOption-activeBackground); - outline: 1px solid var(--vscode-inputOption-activeBorder); -} - -/* decoration styles */ - -.monaco-editor .inline-chat-inserted-range { - background-color: var(--vscode-inlineChatDiff-inserted); -} - -.monaco-editor .inline-chat-inserted-range-linehighlight { - background-color: var(--vscode-diffEditor-insertedLineBackground); -} - -.monaco-editor .inline-chat-original-zone2 { - background-color: var(--vscode-diffEditor-removedLineBackground); - opacity: 0.8; -} - -.monaco-editor .inline-chat-lines-inserted-range { - background-color: var(--vscode-diffEditor-insertedTextBackground); -} - -.monaco-editor .inline-chat-block-selection { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -.monaco-editor .inline-chat-slash-command { - opacity: 0; -} - -.monaco-editor .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 { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -/* create zone */ - -.monaco-editor .inline-chat-newfile-widget { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -.monaco-editor .inline-chat-newfile-widget .title { - display: flex; - align-items: center; - justify-content: space-between; -} - -.monaco-editor .inline-chat-newfile-widget .title .detail { - margin-left: 4px; -} - -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget { - display: flex; - margin-left: auto; - margin-right: 8px; -} - -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { - display: inline-flex; - white-space: nowrap; - margin-left: 4px; -} - -/* gutter decoration */ - -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, -.monaco-editor .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 { - opacity: 0.5; -} - -.monaco-editor .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 { - opacity: 1; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 8deb70a073e..d0afbf5d3be 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -5,14 +5,15 @@ 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 { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +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_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_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, 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_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, ACTION_TOGGLE_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; -import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { Action2, IAction2Options, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -30,11 +31,14 @@ 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'; +import { CONTEXT_CHAT_LOCATION, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; 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 @@ -128,10 +132,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) { @@ -149,32 +171,17 @@ export abstract class AbstractInlineChatAction extends EditorAction2 { } -export class MakeRequestAction extends AbstractInlineChatAction { +const CHAT_REGENERATE_MENU = MenuId.for('inlineChat.response.rerun'); - constructor() { - super({ - id: 'inlineChat.accept', - title: localize('accept', 'Make Request'), - icon: Codicon.send, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EMPTY.negate()), - keybinding: { - when: CTX_INLINE_CHAT_FOCUSED, - weight: KeybindingWeight.EditorCore + 7, - primary: KeyCode.Enter - }, - menu: { - id: MENU_INLINE_CHAT_INPUT, - group: 'main', - order: 1, - when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.isEqualTo(false) - } - }); - } +MenuRegistry.appendMenuItem(MenuId.ChatMessageTitle, { + submenu: CHAT_REGENERATE_MENU, + title: localize('reunmenu', "Regenerate..."), + icon: Codicon.refresh, + group: 'navigation', + order: -10, + when: ContextKeyExpr.and(CONTEXT_RESPONSE, CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor)) +}); - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.acceptInput(); - } -} export class ReRunRequestAction extends AbstractInlineChatAction { @@ -185,43 +192,38 @@ export class ReRunRequestAction extends AbstractInlineChatAction { shortTitle: localize('rerunShort', 'Regenerate'), icon: Codicon.refresh, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EMPTY.negate(), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '2_feedback', - order: 3, - } + menu: [{ + id: CHAT_REGENERATE_MENU, + group: 'navigation', + order: -120, + when: ContextKeyExpr.and(CONTEXT_RESPONSE, CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor)) + }] }); } override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): void { - ctrl.regenerate(); + ctrl.rerun({ retry: true }); } - } -export class StopRequestAction extends AbstractInlineChatAction { +export class ReRunRequestWithIntentDetectionAction extends AbstractInlineChatAction { constructor() { super({ - id: 'inlineChat.stop', - title: localize('stop', 'Stop Request'), - icon: Codicon.debugStop, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EMPTY.negate(), CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST), + id: 'inlineChat.rerunWithIntentDetection', + title: localize('rerunWithout', 'Regenerate without Command Detection'), + icon: Codicon.debugRestartFrame, menu: { - id: MENU_INLINE_CHAT_INPUT, - group: 'main', - order: 1, - when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST - }, - keybinding: { - weight: KeybindingWeight.EditorContrib, - primary: KeyCode.Escape + id: CHAT_REGENERATE_MENU, + group: 'navigation', + order: -100, + when: ContextKeyExpr.and(CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE, CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor)) } }); } - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.cancelCurrentRequest(); + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): void { + ctrl.rerun({ withoutIntentDetection: true }); } } @@ -287,44 +289,6 @@ export class FocusInlineChat extends EditorAction2 { } } -export class PreviousFromHistory extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.previousFromHistory', - title: localize('previousFromHistory', 'Previous From History'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_START), - keybinding: { - weight: KeybindingWeight.EditorCore + 10, // win against core_command - primary: KeyCode.UpArrow, - } - }); - } - - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.populateHistory(true); - } -} - -export class NextFromHistory extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.nextFromHistory', - title: localize('nextFromHistory', 'Next From History'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END), - keybinding: { - weight: KeybindingWeight.EditorCore + 10, // win against core_command - primary: KeyCode.DownArrow, - } - }); - } - - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.populateHistory(false); - } -} - export class DiscardHunkAction extends AbstractInlineChatAction { constructor() { @@ -368,7 +332,7 @@ export class DiscardAction extends AbstractInlineChatAction { icon: Codicon.discard, precondition: CTX_INLINE_CHAT_VISIBLE, keybinding: { - weight: KeybindingWeight.EditorContrib, + weight: KeybindingWeight.EditorContrib - 1, primary: KeyCode.Escape, when: CTX_INLINE_CHAT_USER_DID_EDIT.negate() }, @@ -443,16 +407,16 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { constructor() { super({ - id: 'inlineChat.toggleDiff', + id: ACTION_TOGGLE_DIFF, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - title: localize2('showChanges', 'Show Changes'), + title: localize2('showChanges', 'Toggle Changes'), icon: Codicon.diffSingle, toggled: { condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, }, menu: [ { - id: MENU_INLINE_CHAT_WIDGET_FEEDBACK, + id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '1_main', when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF) } @@ -466,49 +430,7 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { } -export class FeebackHelpfulCommand extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.feedbackHelpful', - title: localize('feedback.helpful', 'Helpful'), - icon: Codicon.thumbsup, - precondition: CTX_INLINE_CHAT_VISIBLE, - toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('helpful'), - menu: { - id: MENU_INLINE_CHAT_WIDGET_FEEDBACK, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty), - group: '2_feedback', - order: 1 - } - }); - } - - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): void { - ctrl.feedbackLast(InlineChatResponseFeedbackKind.Helpful); - } -} - -export class FeebackUnhelpfulCommand extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.feedbackunhelpful', - title: localize('feedback.unhelpful', 'Unhelpful'), - icon: Codicon.thumbsdown, - precondition: CTX_INLINE_CHAT_VISIBLE, - toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('unhelpful'), - menu: { - id: MENU_INLINE_CHAT_WIDGET_FEEDBACK, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty), - group: '2_feedback', - order: 2 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): void { - ctrl.feedbackLast(InlineChatResponseFeedbackKind.Unhelpful); - } -} export class ReportIssueForBugCommand extends AbstractInlineChatAction { constructor() { @@ -522,10 +444,6 @@ export class ReportIssueForBugCommand extends AbstractInlineChatAction { when: ContextKeyExpr.and(CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), group: '2_feedback', order: 3 - }, { - id: MENU_INLINE_CHAT_WIDGET, - group: 'config', - order: 3 }] }); } @@ -547,12 +465,8 @@ export class AcceptChanges extends AbstractInlineChatAction { f1: true, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ContextKeyExpr.or(CTX_INLINE_CHAT_DOCUMENT_CHANGED.toNegated(), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview))), keybinding: [{ - weight: KeybindingWeight.EditorContrib + 10, + weight: KeybindingWeight.WorkbenchContrib + 10, primary: KeyMod.CtrlCmd | KeyCode.Enter, - }, { - primary: KeyCode.Escape, - weight: KeybindingWeight.EditorContrib, - when: CTX_INLINE_CHAT_USER_DID_EDIT }], menu: { when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages)), @@ -575,7 +489,7 @@ export class CancelSessionAction extends AbstractInlineChatAction { id: 'inlineChat.cancel', title: localize('cancel', 'Cancel'), icon: Codicon.clearAll, - precondition: CTX_INLINE_CHAT_VISIBLE, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview)), keybinding: { weight: KeybindingWeight.EditorContrib - 1, primary: KeyCode.Escape @@ -605,18 +519,19 @@ export class CloseAction extends AbstractInlineChatAction { precondition: CTX_INLINE_CHAT_VISIBLE, keybinding: { weight: KeybindingWeight.EditorContrib - 1, - primary: KeyCode.Escape + primary: KeyCode.Escape, + when: CTX_INLINE_CHAT_USER_DID_EDIT.negate() }, menu: { id: MENU_INLINE_CHAT_WIDGET, - group: 'main', - order: 0, + group: 'navigation', + order: 10, } }); } async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - ctrl.finishExistingSession(); + ctrl.cancelSession(); } } @@ -736,46 +651,6 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class ExpandMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.expandMessageAction', - title: localize('expandMessage', 'Show More'), - icon: Codicon.chevronDown, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('cropped')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(true); - } -} - -export class ContractMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.contractMessageAction', - title: localize('contractMessage', 'Show Less'), - icon: Codicon.chevronUp, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('expanded')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(false); - } -} - export class InlineAccessibilityHelpContribution extends Disposable { constructor() { super(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts new file mode 100644 index 00000000000..ac06027fce3 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./media/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 { DisposableStore } from 'vs/base/common/lifecycle'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; +import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { Range } from 'vs/editor/common/core/range'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; + +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; + + private readonly _defaultChatModel: ChatModel; + private readonly _widget: ChatWidget; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService instaService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + + this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, `inlineChatDefaultModel/editorContentWidgetPlaceholder`, undefined)); + + const scopedInstaService = instaService.createChild( + new ServiceCollection([ + IContextKeyService, + this._store.add(contextKeyService.createScoped(this._domNode)) + ]) + ); + + this._widget = scopedInstaService.createInstance( + ChatWidget, + ChatAgentLocation.Editor, + { resource: true }, + { + defaultElementHeight: 32, + editorOverflowWidgetsDomNode: _editor.getOverflowWidgetsDomNode(), + renderStyle: 'compact', + renderInputOnTop: true, + supportsFileReferences: false, + menus: { + telemetrySource: 'inlineChat-content' + }, + filter: _item => false + }, + { + listForeground: editorForeground, + listBackground: inlineChatBackground, + inputEditorBackground: inputBackground, + resultEditorBackground: editorBackground + } + ); + this._store.add(this._widget); + this._widget.render(this._inputContainer); + this._widget.setModel(this._defaultChatModel, {}); + this._store.add(this._widget.inputEditor.onDidContentSizeChange(() => _editor.layoutContentWidget(this))); + + this._domNode.tabIndex = -1; + this._domNode.className = 'inline-chat-content-widget interactive-session'; + + this._domNode.appendChild(this._inputContainer); + + this._messageContainer.classList.add('hidden', 'message'); + this._domNode.appendChild(this._messageContainer); + + + const tracker = dom.trackFocus(this._domNode); + this._store.add(tracker.onDidBlur(() => { + if (this._visible + // && !"ON" + ) { + 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] + }; + } + + beforeRender(): IDimension | null { + + const maxHeight = this._widget.input.inputEditor.getOption(EditorOption.lineHeight) * 5; + const inputEditorHeight = this._widget.inputEditor.getContentHeight(); + + this._widget.inputEditor.layout(new dom.Dimension(360, Math.min(maxHeight, inputEditorHeight))); + + // const actualHeight = this._widget.inputPartHeight; + // return new dom.Dimension(width, actualHeight); + return null; + } + + afterRender(): void { + if (this._focusNext) { + this._focusNext = false; + this._widget.focusInput(); + } + } + + // --- + + get chatWidget(): ChatWidget { + return this._widget; + } + + get isVisible(): boolean { + return this._visible; + } + + get value(): string { + return this._widget.inputEditor.getValue(); + } + + show(position: IPosition) { + if (!this._visible) { + this._visible = true; + this._focusNext = true; + + this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position)); + this._widget.inputEditor.setValue(''); + + const wordInfo = this._editor.getModel()?.getWordAtPosition(position); + + this._position = wordInfo ? new Position(position.lineNumber, wordInfo.startColumn) : position; + this._editor.addContentWidget(this); + this._widget.setVisible(true); + } + } + + hide() { + if (this._visible) { + this._visible = false; + this._editor.removeContentWidget(this); + this._widget.saveState(); + this._widget.setVisible(false); + } + } + + setSession(session: Session): void { + this._widget.setModel(session.chatModel, {}); + this._widget.setInputPlaceholder(session.session.placeholder ?? ''); + this._updateMessage(session.session.message ?? ''); + } + + private _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/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 38ea05687a1..ecbce151de7 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -3,27 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Barrier, Queue, raceCancellation, raceCancellationError } from 'vs/base/common/async'; +import { Barrier, DeferredPromise, Queue, raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; -import { generateUuid } from 'vs/base/common/uuid'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { ProviderResult, TextEdit } from 'vs/editor/common/languages'; +import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; +import { CompletionItemKind, CompletionList, TextEdit } from 'vs/editor/common/languages'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { localize } from 'vs/nls'; @@ -32,27 +29,33 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c 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 { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { chatAgentLeader, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } 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, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from './inlineChatSessionService'; -import { EditModeStrategy, IEditObserver, LivePreviewStrategy, 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 { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; +import { CTX_INLINE_CHAT_DID_EDIT, 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, 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 { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; +import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; +import { tail } from 'vs/base/common/arrays'; +import { IChatRequestModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', INIT_UI = 'INIT_UI', WAIT_FOR_INPUT = 'WAIT_FOR_INPUT', - MAKE_REQUEST = 'MAKE_REQUEST', + SHOW_REQUEST = 'SHOW_REQUEST', APPLY_RESPONSE = 'APPLY_RESPONSE', SHOW_RESPONSE = 'SHOW_RESPONSE', PAUSE = 'PAUSE', @@ -68,7 +71,6 @@ const enum Message { CANCEL_REQUEST = 1 << 3, CANCEL_INPUT = 1 << 4, ACCEPT_INPUT = 1 << 5, - RERUN_INPUT = 1 << 6, } export abstract class InlineChatRunOptions { @@ -77,13 +79,12 @@ export abstract class InlineChatRunOptions { message?: string; autoSend?: boolean; existingSession?: Session; - existingExchange?: { prompt: string; response: IInlineChatResponse }; isUnstashed?: boolean; position?: IPosition; withIntentDetection?: boolean; static isInteractiveEditorOptions(options: any): options is InlineChatRunOptions { - const { initialSelection, initialRange, message, autoSend, position, existingExchange, existingSession } = options; + const { initialSelection, initialRange, message, autoSend, position, existingSession } = options; if ( typeof message !== 'undefined' && typeof message !== 'string' || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' @@ -91,7 +92,6 @@ export abstract class InlineChatRunOptions { || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) || typeof position !== 'undefined' && !Position.isIPosition(position) || typeof existingSession !== 'undefined' && !(existingSession instanceof Session) - || typeof existingExchange !== 'undefined' && typeof existingExchange !== 'object' ) { return false; } @@ -105,16 +105,12 @@ 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 _isDisposed: boolean = false; private readonly _store = new DisposableStore(); + private readonly _input: Lazy; private readonly _zone: Lazy; - private readonly _ctxHasActiveRequest: IContextKey; + + private readonly _ctxVisible: IContextKey; private readonly _ctxResponseTypes: IContextKey; private readonly _ctxDidEdit: IContextKey; private readonly _ctxUserDidEdit: IContextKey; @@ -134,6 +130,9 @@ export class InlineChatController implements IEditorContribution { private _session?: Session; private _strategy?: EditModeStrategy; + private _nextAttempt: number = 0; + private _nextWithIntentDetection: boolean = true; + constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @@ -144,18 +143,21 @@ export class InlineChatController implements IEditorContribution { @IConfigurationService private readonly _configurationService: IConfigurationService, @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, - @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IChatService private readonly _chatService: IChatService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, - @IStorageService private readonly _storageService: IStorageService, @ICommandService private readonly _commandService: ICommandService, + @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, ) { - this._ctxHasActiveRequest = CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.bindTo(contextKeyService); + this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.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._input = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatContentWidget, this._editor))); this._zone = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatZoneWidget, this._editor))); this._store.add(this._editor.onDidChangeModel(async e => { @@ -190,18 +192,6 @@ 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); - }; } dispose(): void { @@ -251,8 +241,6 @@ export class InlineChatController implements IEditorContribution { if (options.initialSelection) { this._editor.setSelection(options.initialSelection); } - this._historyOffset = -1; - this._historyCandidate = ''; this._stashedSession.clear(); this._onWillStartSession.fire(); this._currentRun = this._nextState(State.CREATE_SESSION, options); @@ -294,9 +282,10 @@ export class InlineChatController implements IEditorContribution { delete options.position; } - this._showWidget(true, initPosition); + const widgetPosition = this._showWidget(true, initPosition); - this._updatePlaceholder(); + // this._updatePlaceholder(); + let errorMessage = localize('create.fail', "Failed to start editor chat"); if (!session) { const createSessionCts = new CancellationTokenSource(); @@ -312,11 +301,18 @@ export class InlineChatController implements IEditorContribution { } }); - session = await this._inlineChatSessionService.createSession( - this._editor, - { editMode: this._getMode(), wholeRange: options.initialRange }, - createSessionCts.token - ); + try { + session = await this._inlineChatSessionService.createSession( + this._editor, + { editMode: this._getMode(), wholeRange: options.initialRange }, + createSessionCts.token + ); + } catch (error) { + // Inline chat errors are from the provider and have their error messages shown to the user + if (error instanceof InlineChatError || error?.name === InlineChatError.code) { + errorMessage = error.message; + } + } createSessionCts.dispose(); msgListener.dispose(); @@ -333,7 +329,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(errorMessage, widgetPosition); + this._log('Failed to start editor chat'); return State.CANCEL; } @@ -342,15 +339,15 @@ export class InlineChatController implements IEditorContribution { case EditMode.Preview: this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._editor, this._zone.value); break; - case EditMode.LivePreview: - this._strategy = this._instaService.createInstance(LivePreviewStrategy, session, this._editor, this._zone.value); - break; case EditMode.Live: default: this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._zone.value); break; } + if (session.session.input) { + options.message = session.session.input; + } this._session = session; return State.INIT_UI; } @@ -364,12 +361,6 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.clear(); - this._sessionStore.add(this._zone.value.widget.onRequestWithoutIntentDetection(async () => { - options.withIntentDetection = false; - - this.regenerate(); - })); - const wholeRangeDecoration = this._editor.createDecorationsCollection(); const updateWholeRangeDecoration = () => { const newDecorations = this._strategy?.getWholeRangeDecoration() ?? []; @@ -379,16 +370,17 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.add(this._session.wholeRange.onDidChange(updateWholeRangeDecoration)); updateWholeRangeDecoration(); - this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); + this._sessionStore.add(this._input.value.onDidBlur(() => this.cancelSession())); + + this._input.value.setSession(this._session); + // this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); - this._zone.value.widget.updateInfo(this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._zone.value.widget.preferredExpansionState = this._session.lastExpansionState; - this._zone.value.widget.value = this._session.session.input ?? this._session.lastInput?.value ?? this._zone.value.widget.value; - if (this._session.session.input) { - this._zone.value.widget.selectAll(); - } + const message = this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"); + + + this._zone.value.widget.updateInfo(message); - this._showWidget(true); + this._showWidget(!this._session.lastExchange); this._sessionStore.add(this._editor.onDidChangeModel((e) => { const msg = this._session?.lastExchange @@ -426,9 +418,107 @@ export class InlineChatController implements IEditorContribution { } })); + this._sessionStore.add(this._session.chatModel.onDidChange(e => { + if (e.kind === 'addRequest' && e.request.response) { + this._zone.value.widget.updateProgress(true); + + const listener = e.request.response.onDidChange(() => { + + if (e.request.response?.isCanceled || e.request.response?.isComplete) { + this._zone.value.widget.updateProgress(false); + listener.dispose(); + } + }); + } + })); + // Update context key this._ctxSupportIssueReporting.set(this._session.provider.supportIssueReporting ?? false); + // #region DEBT + // DEBT@jrieken + // REMOVE when agents are adopted + this._sessionStore.add(this._languageFeatureService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'inline chat commands', + triggerCharacters: ['/'], + provideCompletionItems: (model, position, context, token) => { + if (position.lineNumber !== 1) { + return undefined; + } + if (!this._session || !this._session.session.slashCommands) { + return undefined; + } + const widget = this._chatWidgetService.getWidgetByInputUri(model.uri); + if (widget !== this._zone.value.widget.chatWidget && widget !== this._input.value.chatWidget) { + return undefined; + } + + const result: CompletionList = { suggestions: [], incomplete: false }; + for (const command of this._session.session.slashCommands) { + const withSlash = `/${command.command}`; + result.suggestions.push({ + label: { label: withSlash, description: command.detail ?? '' }, + kind: CompletionItemKind.Text, + insertText: withSlash, + range: Range.fromPositions(new Position(1, 1), position), + command: command.executeImmediately ? { id: 'workbench.action.chat.acceptInput', title: withSlash } : undefined + }); + } + + return result; + } + })); + + const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => { + + const newDecorations: IModelDeltaDecoration[] = []; + for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.command.length - a.command.length)) { + const withSlash = `/${command.command}`; + const firstLine = model.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: ' ' + } + } + }); + + // inject detail when otherwise empty + if (firstLine.trim() === `/${command.command}`) { + newDecorations.push({ + range: new Range(1, withSlash.length, 1, withSlash.length), + options: { + description: 'inline-chat-slash-command-detail', + after: { + content: `${command.detail}`, + inlineClassName: 'inline-chat-slash-command-detail' + } + } + }); + } + break; + } + } + collection.set(newDecorations); + }; + const inputInputEditor = this._input.value.chatWidget.inputEditor; + const zoneInputEditor = this._zone.value.widget.chatWidget.inputEditor; + const inputDecorations = inputInputEditor.createDecorationsCollection(); + const zoneDecorations = zoneInputEditor.createDecorationsCollection(); + this._sessionStore.add(inputInputEditor.onDidChangeModelContent(() => updateSlashDecorations(inputDecorations, inputInputEditor.getModel()!))); + this._sessionStore.add(zoneInputEditor.onDidChangeModelContent(() => updateSlashDecorations(zoneDecorations, zoneInputEditor.getModel()!))); + this._sessionStore.add(toDisposable(() => { + inputDecorations.clear(); + zoneDecorations.clear(); + })); + + //#endregion ------- DEBT + if (!this._session.lastExchange) { return State.WAIT_FOR_INPUT; } else if (options.isUnstashed) { @@ -439,42 +529,48 @@ export class InlineChatController implements IEditorContribution { } } - private async [State.WAIT_FOR_INPUT](options: InlineChatRunOptions): Promise { + private async [State.WAIT_FOR_INPUT](options: InlineChatRunOptions): Promise { assertType(this._session); assertType(this._strategy); this._updatePlaceholder(); - if (options.existingExchange) { - options.message = options.existingExchange.prompt; - options.autoSend = true; - } - if (options.message) { this.updateInput(options.message); aria.alert(options.message); delete options.message; + this._showWidget(false); } let message = Message.NONE; + let request: IChatRequestModel | undefined; + + const barrier = new Barrier(); + const store = new DisposableStore(); + store.add(this._session.chatModel.onDidChange(e => { + if (e.kind === 'addRequest') { + request = e.request; + message = Message.ACCEPT_INPUT; + barrier.open(); + } + })); + store.add(this._strategy.onDidAccept(() => this.acceptSession())); + store.add(this._strategy.onDidDiscard(() => this.cancelSession())); + store.add(Event.once(this._messages.event)(m => { + this._log('state=_waitForInput) message received', m); + message = m; + barrier.open(); + })); + if (options.autoSend) { - message = Message.ACCEPT_INPUT; delete options.autoSend; - - } else { - const barrier = new Barrier(); - const store = new DisposableStore(); - store.add(this._strategy.onDidAccept(() => this.acceptSession())); - store.add(this._strategy.onDidDiscard(() => this.cancelSession())); - store.add(Event.once(this._messages.event)(m => { - this._log('state=_waitForInput) message received', m); - message = m; - barrier.open(); - })); - await barrier.wait(); - store.dispose(); + this._showWidget(false); + this._zone.value.widget.chatWidget.acceptInput(); } + await barrier.wait(); + store.dispose(); + if (message & (Message.CANCEL_INPUT | Message.CANCEL_SESSION)) { return State.CANCEL; @@ -489,245 +585,165 @@ export class InlineChatController implements IEditorContribution { return State.ACCEPT; } - if (message & Message.RERUN_INPUT && this._session.lastExchange) { - const { lastExchange } = this._session; - if (options.withIntentDetection === undefined) { // @ulugbekna: if we're re-running with intent detection turned off, no need to update `attempt` # - this._session.addInput(lastExchange.prompt.retry()); - } - if (lastExchange.response instanceof ReplyResponse) { - try { - this._session.hunkData.ignoreTextModelNChanges = true; - await this._strategy.undoChanges(lastExchange.response.modelAltVersionId); - } finally { - this._session.hunkData.ignoreTextModelNChanges = false; - } - } - return State.MAKE_REQUEST; - } - - if (!this.getInput()) { + if (!request?.message.text) { return State.WAIT_FOR_INPUT; } - const input = this.getInput(); + const input = request.message.text; + this._zone.value.widget.value = input; + // slash command referring + let slashCommandLike = request.message.parts.find(part => part instanceof ChatRequestAgentSubcommandPart || part instanceof ChatRequestSlashCommandPart); + const refer = this._session.session.slashCommands?.some(value => { + if (value.refer) { + if (slashCommandLike?.text === `/${value.command}`) { + return true; + } + if (request?.message.text.startsWith(`/${value.command}`)) { + slashCommandLike = new ChatRequestSlashCommandPart(new OffsetRange(0, 1), new Range(1, 1, 1, 1), { command: value.command, detail: value.detail ?? '' }); + return true; + } + } + return false; + }); + if (refer && slashCommandLike && !this._session.lastExchange) { + this._log('[IE] seeing refer command, continuing outside editor', this._session.provider.extensionId); - this._historyUpdate(input); + // cancel this request + this._chatService.cancelCurrentRequestForSession(request.session.sessionId); - const refer = this._session.session.slashCommands?.some(value => value.refer && input.startsWith(`/${value.command}`)); - if (refer) { - this._log('[IE] seeing refer command, continuing outside editor', this._session.provider.debugName); this._editor.setSelection(this._session.wholeRange.value); let massagedInput = input; - if (input.startsWith(chatSubcommandLeader)) { - 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); + const withoutSubCommandLeader = slashCommandLike.text.slice(1); + for (const agent of this._chatAgentService.getActivatedAgents()) { + if (agent.locations.includes(ChatAgentLocation.Panel)) { + const commands = agent.slashCommands; if (commands.find((command) => withoutSubCommandLeader.startsWith(command.name))) { - massagedInput = `${chatAgentLeader}${agent.id} ${input}`; + massagedInput = `${chatAgentLeader}${agent.name} ${slashCommandLike.text}`; break; } } } // if agent has a refer command, massage the input to include the agent name - this._instaService.invokeFunction(sendRequest, massagedInput); + await this._instaService.invokeFunction(sendRequest, massagedInput); - if (!this._session.lastExchange) { - // DONE when there wasn't any exchange yet. We used the inline chat only as trampoline - return State.ACCEPT; - } - return State.WAIT_FOR_INPUT; + return State.ACCEPT; } - this._session.addInput(new SessionPrompt(input)); - return State.MAKE_REQUEST; + this._session.addInput(new SessionPrompt(input, this._nextAttempt, this._nextWithIntentDetection)); + + // we globally store the next attempt and intent detection flag + // to be able to use it in the next request. This is because they + // aren't part of the chat widget state and we need to remembered here + this._nextAttempt = 0; + this._nextWithIntentDetection = true; + + + return State.SHOW_REQUEST; } - private async [State.MAKE_REQUEST](options: InlineChatRunOptions): Promise { - assertType(this._editor.hasModel()); + + private async [State.SHOW_REQUEST](options: InlineChatRunOptions): Promise { assertType(this._session); - assertType(this._strategy); assertType(this._session.lastInput); - const requestCts = new CancellationTokenSource(); + const request: IChatRequestModel | undefined = tail(this._session.chatModel.getRequests()); - let message = Message.NONE; - const msgListener = Event.once(this._messages.event)(m => { - this._log('state=_makeRequest) message received', m); - message = m; - requestCts.cancel(); - }); + assertType(request); + assertType(request.response); - const typeListener = this._zone.value.widget.onDidChangeInput(() => requestCts.cancel()); - - const requestClock = StopWatch.create(); - const request: IInlineChatRequest = { - requestId: generateUuid(), - prompt: this._session.lastInput.value, - attempt: this._session.lastInput.attempt, - selection: this._editor.getSelection(), - wholeRange: this._session.wholeRange.trackedInitialRange, - live: this._session.editMode !== EditMode.Preview, // TODO@jrieken let extension know what document is used for previewing - previewDocument: this._session.textModelN.uri, - withIntentDetection: options.withIntentDetection ?? true /* use intent detection by default */, - }; + this._showWidget(false); + this._zone.value.widget.value = request.message.text; + this._zone.value.widget.selectAll(false); + this._zone.value.widget.updateInfo(''); - // re-enable intent detection - delete options.withIntentDetection; + const { response } = request; + const responsePromise = new DeferredPromise(); - const modelAltVersionIdNow = this._session.textModelN.getAlternativeVersionId(); - const progressEdits: TextEdit[][] = []; + const store = new DisposableStore(); + const progressiveEditsCts = store.add(new CancellationTokenSource()); const progressiveEditsAvgDuration = new MovingAverage(); - const progressiveEditsCts = new CancellationTokenSource(requestCts.token); const progressiveEditsClock = StopWatch.create(); const progressiveEditsQueue = new Queue(); - let progressiveChatResponse: IInlineChatMessageAppender | undefined; + let lastLength = 0; - const progress = new Progress(data => { - this._log('received chunk', data, request); - if (requestCts.token.isCancellationRequested) { - return; - } + let message = Message.NONE; + store.add(Event.once(this._messages.event)(m => { + this._log('state=_makeRequest) message received', m); + this._chatService.cancelCurrentRequestForSession(request.session.sessionId); + message = m; + })); - if (data.message) { - this._zone.value.widget.updateToolbar(false); - this._zone.value.widget.updateInfo(data.message); - } - if (data.slashCommand) { - const valueNow = this.getInput(); - if (!valueNow.startsWith('/')) { - this._zone.value.widget.updateSlashCommandUsed(data.slashCommand); - } - } - if (data.edits?.length) { - if (!request.live) { - throw new Error('Progress in NOT supported in non-live mode'); - } - progressEdits.push(data.edits); - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); - - progressiveEditsQueue.queue(async () => { - - const startThen = this._session!.wholeRange.value.getStartPosition(); - - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - await this._makeChanges(data.edits!, data.editsShouldBeInstant - ? undefined - : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } - ); - - // reshow the widget if the start position changed or shows at the wrong position - const startNow = this._session!.wholeRange.value.getStartPosition(); - if (!startNow.equals(startThen) || !this._zone.value.position?.equals(startNow)) { - this._showWidget(false, startNow.delta(-1)); - } - }); - } - if (data.markdownFragment) { - if (!progressiveChatResponse) { - const message = { - message: new MarkdownString(data.markdownFragment, { supportThemeIcons: true, supportHtml: true, isTrusted: false }), - providerId: this._session!.provider.debugName, - requestId: request.requestId, - }; - progressiveChatResponse = this._zone.value.widget.updateChatMessage(message, true); - } else { - progressiveChatResponse.appendContent(data.markdownFragment); - } - } - }); + // cancel the request when the user types + store.add(this._zone.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { + this._chatService.cancelCurrentRequestForSession(request.session.sessionId); + })); - let a11yResponse: string | undefined; - const a11yVerboseInlineChat = this._configurationService.getValue('accessibility.verbosity.inlineChat') === true; - const requestId = this._chatAccessibilityService.acceptRequest(); - let task: ProviderResult; - if (options.existingExchange) { - task = options.existingExchange.response; - delete options.existingExchange; - this._log('using READY-response', this._session.provider.debugName, this._session.session); - } else { - task = this._session.provider.provideResponse(this._session.session, request, progress, requestCts.token); - this._log('request started', this._session.provider.debugName, this._session.session, request); - } + // apply edits + store.add(response.onDidChange(() => { - let response: ReplyResponse | ErrorResponse | EmptyResponse; - let reply: IInlineChatResponse | null | undefined; - try { - this._zone.value.widget.updateChatMessage(undefined); - this._zone.value.widget.updateFollowUps(undefined); - this._zone.value.widget.updateProgress(true); - this._zone.value.widget.updateInfo(!this._session.lastExchange ? localize('thinking', "Thinking\u2026") : ''); - this._ctxHasActiveRequest.set(true); - reply = await raceCancellationError(Promise.resolve(task), requestCts.token); - - // we must wait for all edits that came in via progress to complete - await progressiveEditsQueue.whenIdle(); - - if (progressiveChatResponse) { - progressiveChatResponse.cancel(); + if (response.isCanceled) { + progressiveEditsCts.cancel(); + responsePromise.complete(); + return; } - if (!reply) { - response = new EmptyResponse(); - a11yResponse = localize('empty', "No results, please refine your input and try again"); - } else { - const markdownContents = reply.message ?? new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const replyResponse = response = this._instaService.createInstance(ReplyResponse, reply, markdownContents, this._session.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId); - - for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { - await this._makeChanges(replyResponse.allLocalEdits[i], undefined); - } + if (response.isComplete) { + responsePromise.complete(); + return; + } - const a11yMessageResponse = renderMarkdownAsPlaintext(replyResponse.mdContent); + // TODO@jrieken + const editsShouldBeInstant = false; - a11yResponse = a11yVerboseInlineChat - ? a11yMessageResponse ? localize('editResponseMessage2', "{0}, also review proposed changes in the diff editor.", a11yMessageResponse) : localize('editResponseMessage', "Review proposed changes in the diff editor.") - : a11yMessageResponse; + const edits = response.edits.get(this._session!.textModelN.uri) ?? []; + const newEdits = edits.slice(lastLength); + // console.log('NEW edits', newEdits, edits); + if (newEdits.length === 0) { + return; // NO change } + lastLength = edits.length; + progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); + progressiveEditsClock.reset(); + + progressiveEditsQueue.queue(async () => { + + const startThen = this._session!.wholeRange.value.getStartPosition(); + + // making changes goes into a queue because otherwise the async-progress time will + // influence the time it takes to receive the changes and progressive typing will + // become infinitely fast + await this._makeChanges(newEdits, editsShouldBeInstant + ? undefined + : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } + ); + + // reshow the widget if the start position changed or shows at the wrong position + const startNow = this._session!.wholeRange.value.getStartPosition(); + if (!startNow.equals(startThen) || !this._zone.value.position?.equals(startNow)) { + this._showWidget(false, startNow.delta(-1)); + } + }); + })); - } catch (e) { - progressiveEditsQueue.clear(); - response = new ErrorResponse(e); - a11yResponse = (response).message; + // (1) we must wait for the request to finish + // (2) we must wait for all edits that came in via progress to complete + await responsePromise.p; + await progressiveEditsQueue.whenIdle(); - } finally { - this._ctxHasActiveRequest.set(false); - this._zone.value.widget.updateProgress(false); - this._zone.value.widget.updateInfo(''); - this._zone.value.widget.updateToolbar(true); - this._log('request took', requestClock.elapsed(), this._session.provider.debugName); - this._chatAccessibilityService.acceptResponse(a11yResponse, requestId); - } + store.dispose(); // todo@jrieken we can likely remove 'trackEdit' const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); this._session.wholeRange.fixup(diff?.changes ?? []); - progressiveEditsCts.dispose(true); - requestCts.dispose(); - msgListener.dispose(); - typeListener.dispose(); + await this._session.hunkData.recompute(); - if (response instanceof ReplyResponse) { - // update hunks after a reply response - await this._session.hunkData.recompute(); - - } else if (request.live) { - // undo changes that might have been made when not - // having a reply response - this._strategy?.undoChanges(modelAltVersionIdNow); - } - - this._session.addExchange(new SessionExchange(this._session.lastInput, response)); + this._zone.value.widget.updateToolbar(true); if (message & Message.CANCEL_SESSION) { return State.CANCEL; @@ -735,8 +751,6 @@ export class InlineChatController implements IEditorContribution { return State.PAUSE; } else if (message & Message.ACCEPT_SESSION) { return State.ACCEPT; - } else if (message & (Message.ACCEPT_INPUT | Message.RERUN_INPUT)) { - return State.MAKE_REQUEST; } else { return State.APPLY_RESPONSE; } @@ -795,11 +809,6 @@ export class InlineChatController implements IEditorContribution { } else if (response instanceof ReplyResponse) { // real response -> complex... this._zone.value.widget.updateStatus(''); - 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); @@ -810,10 +819,10 @@ export class InlineChatController implements IEditorContribution { followupCts.cancel(); }); const followupTask = this._session.provider.provideFollowups(this._session.session, response.raw, followupCts.token); - this._log('followup request started', this._session.provider.debugName, this._session.session, response.raw); + this._log('followup request started', this._session.provider.extensionId, this._session.session, response.raw); raceCancellation(Promise.resolve(followupTask), followupCts.token).then(followupReply => { if (followupReply && this._session) { - this._log('followup request received', this._session.provider.debugName, this._session.session, followupReply); + this._log('followup request received', this._session.provider.extensionId, this._session.session, followupReply); this._zone.value.widget.updateFollowUps(followupReply, followup => { if (followup.kind === 'reply') { this.updateInput(followup.message); @@ -905,44 +914,62 @@ export class InlineChatController implements IEditorContribution { if (position) { // explicit position wins widgetPosition = position; - } else if (this._zone.value.position) { + } else if (this._zone.rawValue?.position) { // already showing - special case of line 1 - if (this._zone.value.position.lineNumber === 1) { - widgetPosition = this._zone.value.position.delta(-1); + if (this._zone.rawValue.position.lineNumber === 1) { + widgetPosition = this._zone.rawValue.position.delta(-1); } else { - widgetPosition = this._zone.value.position; + widgetPosition = this._zone.rawValue.position; } } else { // default to ABOVE the selection 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); - } else { - this._zone.value.setWidgetMargins(widgetPosition); + + if (this._zone.rawValue?.position) { this._zone.value.updatePositionAndHeight(widgetPosition); + + } else if (initialRender) { + const selection = this._editor.getSelection(); + widgetPosition = selection.getStartPosition(); + // TODO@jrieken we are not ready for this + // widgetPosition = selection.getEndPosition(); + // if (Range.spansMultipleLines(selection) && widgetPosition.column === 1) { + // // selection ends on "nothing" -> move up to match the + // // rendered/visible part of the selection + // widgetPosition = this._editor.getModel().validatePosition(widgetPosition.delta(-1, Number.MAX_SAFE_INTEGER)); + // } + this._input.value.show(widgetPosition); + + } else { + this._input.value.hide(); + this._zone.value.show(widgetPosition); + if (this._session && this._zone.value.widget.chatWidget.viewModel?.model !== this._session.chatModel) { + this._zone.value.widget.setChatModel(this._session.chatModel); + } + } + + if (this._session && this._zone.rawValue) { + this._zone.rawValue.updateBackgroundColor(widgetPosition, this._session.wholeRange.value); } + + 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 @@ -956,7 +983,7 @@ export class InlineChatController implements IEditorContribution { assertType(this._strategy); const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits); - this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.provider.debugName, edits, moreMinimalEdits); + this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.provider.extensionId, edits, moreMinimalEdits); if (moreMinimalEdits?.length === 0) { // nothing left to do @@ -1010,22 +1037,59 @@ export class InlineChatController implements IEditorContribution { } acceptInput(): void { - this._messages.fire(Message.ACCEPT_INPUT); + if (this._input.value.isVisible) { + this._input.value.chatWidget.acceptInput(); + } else { + this._zone.value.widget.chatWidget.acceptInput(); + } } updateInput(text: string, selectAll = true): void { - this._zone.value.widget.value = text; + + this._input.value.chatWidget.setInput(text); + this._zone.value.widget.chatWidget.setInput(text); if (selectAll) { - this._zone.value.widget.selectAll(); + const newSelection = new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1); + this._input.value.chatWidget.inputEditor.setSelection(newSelection); + this._zone.value.widget.chatWidget.inputEditor.setSelection(newSelection); } } getInput(): string { - return this._zone.value.widget.value; + return this._input.value.isVisible + ? this._input.value.value + : this._zone.value.widget.value; } - regenerate(): void { - this._messages.fire(Message.RERUN_INPUT); + async rerun(opts: { retry?: boolean; withoutIntentDetection?: boolean }) { + if (this._session?.lastExchange && this._strategy) { + const { lastExchange } = this._session; + + const request = tail(this._session.chatModel.getRequests()); + if (!request || !request.response?.isComplete) { + return; + } + + this._session.chatModel.removeRequest(request.id); + + if (lastExchange.response instanceof ReplyResponse) { + try { + this._session.hunkData.ignoreTextModelNChanges = true; + await this._strategy.undoChanges(lastExchange.response.modelAltVersionId); + } finally { + this._session.hunkData.ignoreTextModelNChanges = false; + } + } + + if (opts.retry) { + this._nextAttempt = lastExchange.prompt.attempt + 1; + } + if (opts.withoutIntentDetection) { + this._nextWithIntentDetection = false; + } + + this._zone.value.widget.chatWidget.acceptInput(request.message.text); + } } cancelCurrentRequest(): void { @@ -1055,35 +1119,6 @@ export class InlineChatController implements IEditorContribution { this._strategy?.move?.(next); } - 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; - } - - this._zone.value.widget.value = entry; - this._zone.value.widget.selectAll(); - } viewInChat() { if (this._session?.lastExchange?.response instanceof ReplyResponse) { @@ -1091,14 +1126,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?.(); } @@ -1185,10 +1212,14 @@ export class InlineChatController implements IEditorContribution { async function showMessageResponse(accessor: ServicesAccessor, query: string, response: string) { const chatService = accessor.get(IChatService); - const providerId = chatService.getProviderInfos()[0]?.id; + const chatAgentService = accessor.get(IChatAgentService); + const agent = chatAgentService.getActivatedAgents().find(agent => agent.locations.includes(ChatAgentLocation.Panel) && agent.isDefault); + if (!agent) { + return; + } const chatWidgetService = accessor.get(IChatWidgetService); - const widget = await chatWidgetService.revealViewForProvider(providerId); + const widget = await chatWidgetService.revealViewForProvider(agent.name); if (widget && widget.viewModel) { chatService.addCompleteRequest(widget.viewModel.sessionId, query, undefined, { message: response }); widget.focusLastMessage(); @@ -1196,14 +1227,16 @@ async function showMessageResponse(accessor: ServicesAccessor, query: string, re } async function sendRequest(accessor: ServicesAccessor, query: string) { - const chatService = accessor.get(IChatService); const widgetService = accessor.get(IChatWidgetService); - - const providerId = chatService.getProviderInfos()[0]?.id; - const widget = await widgetService.revealViewForProvider(providerId); + const chatAgentService = accessor.get(IChatAgentService); + const agent = chatAgentService.getActivatedAgents().find(agent => agent.locations.includes(ChatAgentLocation.Panel) && agent.isDefault); + if (!agent) { + return; + } + const widget = await widgetService.revealViewForProvider(agent.name); if (!widget) { return; } - + widget.focusInput(); widget.acceptInput(query); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts new file mode 100644 index 00000000000..eca335cb375 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension, h } from 'vs/base/browser/dom'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; +import * as editorColorRegistry from 'vs/editor/common/core/editorColorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { INLINE_CHAT_ID, inlineChatRegionHighlight } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { Position } from 'vs/editor/common/core/position'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { FileKind } from 'vs/platform/files/common/files'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IAction, toAction } from 'vs/base/common/actions'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Codicon } from 'vs/base/common/codicons'; +import { TAB_ACTIVE_MODIFIED_BORDER } from 'vs/workbench/common/theme'; +import { localize } from 'vs/nls'; +import { Event } from 'vs/base/common/event'; + +export class InlineChatFileCreatePreviewWidget extends ZoneWidget { + + private static TitleHeight = 35; + + private readonly _elements = h('div.inline-chat-newfile-widget@domNode', [ + h('div.title@title', [ + h('span.name.show-file-icons@name'), + h('span.detail@detail'), + ]), + h('div.editor@editor'), + ]); + + private readonly _name: ResourceLabel; + private readonly _previewEditor: ICodeEditor; + private readonly _previewStore = new MutableDisposable(); + private readonly _buttonBar: ButtonBarWidget; + private _dim: Dimension | undefined; + + constructor( + parentEditor: ICodeEditor, + @IInstantiationService instaService: IInstantiationService, + @IThemeService themeService: IThemeService, + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IEditorService private readonly _editorService: IEditorService, + ) { + super(parentEditor, { + showArrow: false, + showFrame: true, + frameColor: colorRegistry.asCssVariable(TAB_ACTIVE_MODIFIED_BORDER), + frameWidth: 1, + isResizeable: true, + isAccessible: true, + showInHiddenAreas: true, + ordinal: 10000 + 2 + }); + super.create(); + + this._name = instaService.createInstance(ResourceLabel, this._elements.name, { supportIcons: true }); + this._elements.detail.appendChild(renderIcon(Codicon.circleFilled)); + + const contributions = EditorExtensionsRegistry + .getEditorContributions() + .filter(c => c.id !== INLINE_CHAT_ID); + + this._previewEditor = instaService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, { + scrollBeyondLastLine: false, + stickyScroll: { enabled: false }, + minimap: { enabled: false }, + scrollbar: { alwaysConsumeMouseWheel: false, useShadows: true, ignoreHorizontalScrollbarInContentHeight: true, }, + }, { isSimpleWidget: true, contributions }, parentEditor); + + const doStyle = () => { + const theme = themeService.getColorTheme(); + const overrides: [target: string, source: string][] = [ + [colorRegistry.editorBackground, inlineChatRegionHighlight], + [editorColorRegistry.editorGutter, inlineChatRegionHighlight], + ]; + + for (const [target, source] of overrides) { + const value = theme.getColor(source); + if (value) { + this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value)); + } + } + }; + doStyle(); + this._disposables.add(themeService.onDidColorThemeChange(doStyle)); + + this._buttonBar = instaService.createInstance(ButtonBarWidget); + this._elements.title.appendChild(this._buttonBar.domNode); + } + + override dispose(): void { + this._name.dispose(); + this._buttonBar.dispose(); + this._previewEditor.dispose(); + this._previewStore.dispose(); + super.dispose(); + } + + protected override _fillContainer(container: HTMLElement): void { + container.appendChild(this._elements.domNode); + } + + override show(): void { + throw new Error('Use showFileCreation'); + } + + async showCreation(where: Position, untitledTextModel: IUntitledTextEditorModel): Promise { + + const store = new DisposableStore(); + this._previewStore.value = store; + + this._name.element.setFile(untitledTextModel.resource, { + fileKind: FileKind.FILE, + fileDecorations: { badges: true, colors: true } + }); + + const actionSave = toAction({ + id: '1', + label: localize('save', "Create"), + run: () => untitledTextModel.save({ reason: SaveReason.EXPLICIT }) + }); + const actionSaveAs = toAction({ + id: '2', + label: localize('saveAs', "Create As"), + run: async () => { + const ids = this._editorService.findEditors(untitledTextModel.resource, { supportSideBySide: SideBySideEditor.ANY }); + await this._editorService.save(ids.slice(), { saveAs: true, reason: SaveReason.EXPLICIT }); + } + }); + + this._buttonBar.update([ + [actionSave, actionSaveAs], + [(toAction({ id: '3', label: localize('discard', "Discard"), run: () => untitledTextModel.revert() }))] + ]); + + store.add(Event.any( + untitledTextModel.onDidRevert, + untitledTextModel.onDidSave, + untitledTextModel.onDidChangeDirty, + untitledTextModel.onWillDispose + )(() => this.hide())); + + await untitledTextModel.resolve(); + + const ref = await this._textModelResolverService.createModelReference(untitledTextModel.resource); + store.add(ref); + + const model = ref.object.textEditorModel; + this._previewEditor.setModel(model); + + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + + this._elements.title.style.height = `${InlineChatFileCreatePreviewWidget.TitleHeight}px`; + const titleHightInLines = InlineChatFileCreatePreviewWidget.TitleHeight / lineHeight; + + const maxLines = Math.max(4, Math.floor((this.editor.getLayoutInfo().height / lineHeight) * .33)); + const lines = Math.min(maxLines, model.getLineCount()); + + super.show(where, titleHightInLines + lines); + } + + override hide(): void { + this._previewStore.clear(); + super.hide(); + } + + // --- layout + + protected override revealRange(range: Range, isLastLine: boolean): void { + // ignore + } + + protected override _onWidth(widthInPixel: number): void { + if (this._dim) { + this._doLayout(this._dim.height, widthInPixel); + } + } + + protected override _doLayout(heightInPixel: number, widthInPixel: number): void { + + const { lineNumbersLeft } = this.editor.getLayoutInfo(); + this._elements.title.style.marginLeft = `${lineNumbersLeft}px`; + + const newDim = new Dimension(widthInPixel, heightInPixel); + if (!Dimension.equals(this._dim, newDim)) { + this._dim = newDim; + this._previewEditor.layout(this._dim.with(undefined, this._dim.height - InlineChatFileCreatePreviewWidget.TitleHeight)); + } + } +} + + +class ButtonBarWidget { + + private readonly _domNode = h('div.buttonbar-widget'); + private readonly _buttonBar: ButtonBar; + private readonly _store = new DisposableStore(); + + constructor( + @IContextMenuService private _contextMenuService: IContextMenuService, + ) { + this._buttonBar = new ButtonBar(this.domNode); + + } + + update(allActions: IAction[][]): void { + this._buttonBar.clear(); + let secondary = false; + for (const actions of allActions) { + let btn: IButton; + const [first, ...rest] = actions; + if (!first) { + continue; + } else if (rest.length === 0) { + // single action + btn = this._buttonBar.addButton({ ...defaultButtonStyles, secondary }); + } else { + btn = this._buttonBar.addButtonWithDropdown({ + ...defaultButtonStyles, + addPrimaryActionToDropdown: false, + actions: rest, + contextMenuProvider: this._contextMenuService + }); + } + btn.label = first.label; + this._store.add(btn.onDidClick(() => first.run())); + secondary = true; + } + } + + dispose(): void { + this._buttonBar.dispose(); + this._store.dispose(); + } + + get domNode() { + return this._domNode.root; + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts deleted file mode 100644 index 1af9ecfa2f1..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts +++ /dev/null @@ -1,531 +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 { Dimension, getWindow, h, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { assertType } from 'vs/base/common/types'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { EditorOption, IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { Range } from 'vs/editor/common/core/range'; -import { IModelDecorationOptions, ITextModel } from 'vs/editor/common/model'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; -import * as editorColorRegistry from 'vs/editor/common/core/editorColorRegistry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INLINE_CHAT_ID, inlineChatDiffInserted, inlineChatDiffRemoved, inlineChatRegionHighlight } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { LineRange } from 'vs/editor/common/core/lineRange'; -import { Position } from 'vs/editor/common/core/position'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; -import { ILogService } from 'vs/platform/log/common/log'; -import { invertLineRange, asRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { ResourceLabel } from 'vs/workbench/browser/labels'; -import { FileKind } from 'vs/platform/files/common/files'; -import { HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { generateUuid } from 'vs/base/common/uuid'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IAction, toAction } from 'vs/base/common/actions'; -import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { Codicon } from 'vs/base/common/codicons'; -import { TAB_ACTIVE_MODIFIED_BORDER } from 'vs/workbench/common/theme'; -import { localize } from 'vs/nls'; -import { Event } from 'vs/base/common/event'; - -export class InlineChatLivePreviewWidget extends ZoneWidget { - - private readonly _hideId = `overlayDiff:${generateUuid()}`; - - private readonly _elements = h('div.inline-chat-diff-widget@domNode'); - - private readonly _decorationCollection: IEditorDecorationsCollection; - private readonly _diffEditor: DiffEditorWidget; - - private _dim: Dimension | undefined; - private _isVisible: boolean = false; - - constructor( - editor: ICodeEditor, - private readonly _session: Session, - options: IDiffEditorOptions, - onDidChangeDiff: (() => void) | undefined, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @ILogService private readonly _logService: ILogService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - ) { - super(editor, { showArrow: false, showFrame: false, isResizeable: false, isAccessible: true, allowUnlimitedHeight: true, showInHiddenAreas: true, keepEditorSelection: true, ordinal: 10000 + 1 }); - super.create(); - assertType(editor.hasModel()); - - this._decorationCollection = editor.createDecorationsCollection(); - - const diffContributions = EditorExtensionsRegistry - .getEditorContributions() - .filter(c => c.id !== INLINE_CHAT_ID && c.id !== FoldingController.ID); - - this._diffEditor = instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.domNode, { - scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, - scrollBeyondLastLine: false, - renderMarginRevertIcon: true, - renderOverviewRuler: false, - rulers: undefined, - overviewRulerBorder: undefined, - overviewRulerLanes: 0, - diffAlgorithm: 'advanced', - splitViewDefaultRatio: 0.35, - padding: { top: 0, bottom: 0 }, - folding: false, - diffCodeLens: false, - stickyScroll: { enabled: false }, - minimap: { enabled: false }, - isInEmbeddedEditor: true, - useInlineViewWhenSpaceIsLimited: false, - overflowWidgetsDomNode: editor.getOverflowWidgetsDomNode(), - onlyShowAccessibleDiffViewer: this.accessibilityService.isScreenReaderOptimized(), - ...options - }, { - originalEditor: { contributions: diffContributions }, - modifiedEditor: { contributions: diffContributions } - }, editor); - - this._disposables.add(this._diffEditor); - this._diffEditor.setModel({ original: this._session.textModel0, modified: editor.getModel() }); - this._diffEditor.updateOptions({ - lineDecorationsWidth: editor.getLayoutInfo().decorationsWidth - }); - - if (onDidChangeDiff) { - this._disposables.add(this._diffEditor.onDidUpdateDiff(() => { onDidChangeDiff(); })); - - const render = this._disposables.add(new MutableDisposable()); - this._disposables.add(this._diffEditor.onDidContentSizeChange(e => { - if (!this._isVisible || !e.contentHeightChanged) { - return; - } - render.value = runAtThisOrScheduleAtNextAnimationFrame(getWindow(this._diffEditor.getContainerDomNode()), () => { - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - const heightInLines = e.contentHeight / lineHeight; - this._logService.debug(`[IE] relaying with ${heightInLines} lines height`); - this._relayout(heightInLines); - }); - })); - } - - - const doStyle = () => { - const theme = themeService.getColorTheme(); - const overrides: [target: string, source: string][] = [ - [colorRegistry.editorBackground, inlineChatRegionHighlight], - [editorColorRegistry.editorGutter, inlineChatRegionHighlight], - [colorRegistry.diffInsertedLine, inlineChatDiffInserted], - [colorRegistry.diffInserted, inlineChatDiffInserted], - [colorRegistry.diffRemovedLine, inlineChatDiffRemoved], - [colorRegistry.diffRemoved, inlineChatDiffRemoved], - ]; - - for (const [target, source] of overrides) { - const value = theme.getColor(source); - if (value) { - this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value)); - } - } - }; - doStyle(); - this._disposables.add(themeService.onDidColorThemeChange(doStyle)); - } - - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this._elements.domNode); - } - - // --- show / hide -------------------- - - get isVisible(): boolean { - return this._isVisible; - } - - override hide(): void { - this._decorationCollection.clear(); - this._cleanupFullDiff(); - super.hide(); - this._isVisible = false; - } - - override show(): void { - throw new Error('use showForChanges'); - } - - showForChanges(hunk: HunkInformation): void { - const hasFocus = this._diffEditor.hasTextFocus(); - this._isVisible = true; - - const onlyInserts = hunk.isInsertion(); - - if (onlyInserts || this._session.textModel0.getValueLength() === 0) { - // no change or changes to an empty file - this._logService.debug('[IE] livePreview-mode: no diff'); - this._cleanupFullDiff(); - this._renderInsertWithHighlight(hunk); - } else { - // complex changes - this._logService.debug('[IE] livePreview-mode: full diff'); - this._decorationCollection.clear(); - this._renderChangesWithFullDiff(hunk); - } - - // TODO@jrieken find a better fix for this. this is the challenge: - // the `_updateFromChanges` method invokes show of the zone widget which removes and adds the - // zone and overlay parts. this dettaches and reattaches the dom nodes which means they lose - // focus - if (hasFocus) { - this._diffEditor.focus(); - } - } - - private _renderInsertWithHighlight(hunk: HunkInformation) { - assertType(this.editor.hasModel()); - - const options: IModelDecorationOptions = { - description: 'inline-chat-insert', - showIfCollapsed: false, - isWholeLine: true, - className: 'inline-chat-lines-inserted-range', - }; - - this._decorationCollection.set([{ - range: hunk.getRangesN()[0], - options - }]); - } - - // --- full diff - - private _renderChangesWithFullDiff(hunk: HunkInformation) { - assertType(this.editor.hasModel()); - - const ranges = this._computeHiddenRanges(this._session.textModelN, hunk); - - this._hideEditorRanges(this.editor, [ranges.modifiedHidden]); - this._hideEditorRanges(this._diffEditor.getOriginalEditor(), ranges.originalDiffHidden); - this._hideEditorRanges(this._diffEditor.getModifiedEditor(), ranges.modifiedDiffHidden); - - // this._diffEditor.revealLine(ranges.modifiedHidden.startLineNumber, ScrollType.Immediate); - - const lineCountModified = ranges.modifiedHidden.length; - const lineCountOriginal = ranges.originalHidden.length; - - const heightInLines = Math.max(lineCountModified, lineCountOriginal); - - super.show(ranges.anchor, heightInLines); - this._logService.debug(`[IE] diff SHOWING at ${ranges.anchor} with ${heightInLines} (approx) lines height`); - } - - private _cleanupFullDiff() { - this.editor.setHiddenAreas([], this._hideId); - this._diffEditor.getOriginalEditor().setHiddenAreas([], this._hideId); - this._diffEditor.getModifiedEditor().setHiddenAreas([], this._hideId); - super.hide(); - this._isVisible = false; - } - - private _computeHiddenRanges(model: ITextModel, hunk: HunkInformation) { - - - const modifiedLineRange = LineRange.fromRangeInclusive(hunk.getRangesN()[0]); - let originalLineRange = LineRange.fromRangeInclusive(hunk.getRanges0()[0]); - if (originalLineRange.isEmpty) { - originalLineRange = new LineRange(originalLineRange.startLineNumber, originalLineRange.endLineNumberExclusive + 1); - } - - const originalDiffHidden = invertLineRange(originalLineRange, this._session.textModel0); - const modifiedDiffHidden = invertLineRange(modifiedLineRange, model); - - return { - originalHidden: originalLineRange, - originalDiffHidden, - modifiedHidden: modifiedLineRange, - modifiedDiffHidden, - anchor: new Position(modifiedLineRange.startLineNumber - 1, 1) - }; - } - - private _hideEditorRanges(editor: ICodeEditor, lineRanges: LineRange[]): void { - assertType(editor.hasModel()); - - lineRanges = lineRanges.filter(range => !range.isEmpty); - if (lineRanges.length === 0) { - // todo? - this._logService.debug(`[IE] diff NOTHING to hide for ${editor.getId()} with ${String(editor.getModel()?.uri)}`); - return; - } - - let hiddenRanges: Range[]; - const hiddenLinesCount = lineRanges.reduce((p, c) => p + c.length, 0); // assumes no overlap - if (hiddenLinesCount >= editor.getModel().getLineCount()) { - // TODO: not every line can be hidden, keep the first line around - hiddenRanges = [editor.getModel().getFullModelRange().delta(1)]; - } else { - hiddenRanges = lineRanges.map(lr => asRange(lr, editor.getModel())); - } - editor.setHiddenAreas(hiddenRanges, this._hideId); - this._logService.debug(`[IE] diff HIDING ${hiddenRanges} for ${editor.getId()} with ${String(editor.getModel()?.uri)}`); - } - - protected override revealRange(range: Range, isLastLine: boolean): void { - // ignore - } - - // --- layout ------------------------- - - protected override _onWidth(widthInPixel: number): void { - if (this._dim) { - this._doLayout(this._dim.height, widthInPixel); - } - } - - protected override _doLayout(heightInPixel: number, widthInPixel: number): void { - const newDim = new Dimension(widthInPixel, heightInPixel); - if (!Dimension.equals(this._dim, newDim)) { - this._dim = newDim; - this._diffEditor.layout(this._dim.with(undefined, this._dim.height)); - this._logService.debug('[IE] diff LAYOUT', this._dim); - } - } -} - - -export class InlineChatFileCreatePreviewWidget extends ZoneWidget { - - private static TitleHeight = 35; - - private readonly _elements = h('div.inline-chat-newfile-widget@domNode', [ - h('div.title@title', [ - h('span.name.show-file-icons@name'), - h('span.detail@detail'), - ]), - h('div.editor@editor'), - ]); - - private readonly _name: ResourceLabel; - private readonly _previewEditor: ICodeEditor; - private readonly _previewStore = new MutableDisposable(); - private readonly _buttonBar: ButtonBarWidget; - private _dim: Dimension | undefined; - - constructor( - parentEditor: ICodeEditor, - @IInstantiationService instaService: IInstantiationService, - @IThemeService themeService: IThemeService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, - @IEditorService private readonly _editorService: IEditorService, - ) { - super(parentEditor, { - showArrow: false, - showFrame: true, - frameColor: colorRegistry.asCssVariable(TAB_ACTIVE_MODIFIED_BORDER), - frameWidth: 1, - isResizeable: true, - isAccessible: true, - showInHiddenAreas: true, - ordinal: 10000 + 2 - }); - super.create(); - - this._name = instaService.createInstance(ResourceLabel, this._elements.name, { supportIcons: true }); - this._elements.detail.appendChild(renderIcon(Codicon.circleFilled)); - - const contributions = EditorExtensionsRegistry - .getEditorContributions() - .filter(c => c.id !== INLINE_CHAT_ID); - - this._previewEditor = instaService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, { - scrollBeyondLastLine: false, - stickyScroll: { enabled: false }, - minimap: { enabled: false }, - scrollbar: { alwaysConsumeMouseWheel: false, useShadows: true, ignoreHorizontalScrollbarInContentHeight: true, }, - }, { isSimpleWidget: true, contributions }, parentEditor); - - const doStyle = () => { - const theme = themeService.getColorTheme(); - const overrides: [target: string, source: string][] = [ - [colorRegistry.editorBackground, inlineChatRegionHighlight], - [editorColorRegistry.editorGutter, inlineChatRegionHighlight], - ]; - - for (const [target, source] of overrides) { - const value = theme.getColor(source); - if (value) { - this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value)); - } - } - }; - doStyle(); - this._disposables.add(themeService.onDidColorThemeChange(doStyle)); - - this._buttonBar = instaService.createInstance(ButtonBarWidget); - this._elements.title.appendChild(this._buttonBar.domNode); - } - - override dispose(): void { - this._name.dispose(); - this._buttonBar.dispose(); - this._previewEditor.dispose(); - this._previewStore.dispose(); - super.dispose(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this._elements.domNode); - } - - override show(): void { - throw new Error('Use showFileCreation'); - } - - async showCreation(where: Position, untitledTextModel: IUntitledTextEditorModel): Promise { - - const store = new DisposableStore(); - this._previewStore.value = store; - - this._name.element.setFile(untitledTextModel.resource, { - fileKind: FileKind.FILE, - fileDecorations: { badges: true, colors: true } - }); - - const actionSave = toAction({ - id: '1', - label: localize('save', "Create"), - run: () => untitledTextModel.save({ reason: SaveReason.EXPLICIT }) - }); - const actionSaveAs = toAction({ - id: '2', - label: localize('saveAs', "Create As"), - run: async () => { - const ids = this._editorService.findEditors(untitledTextModel.resource, { supportSideBySide: SideBySideEditor.ANY }); - await this._editorService.save(ids.slice(), { saveAs: true, reason: SaveReason.EXPLICIT }); - } - }); - - this._buttonBar.update([ - [actionSave, actionSaveAs], - [(toAction({ id: '3', label: localize('discard', "Discard"), run: () => untitledTextModel.revert() }))] - ]); - - store.add(Event.any( - untitledTextModel.onDidRevert, - untitledTextModel.onDidSave, - untitledTextModel.onDidChangeDirty, - untitledTextModel.onWillDispose - )(() => this.hide())); - - await untitledTextModel.resolve(); - - const ref = await this._textModelResolverService.createModelReference(untitledTextModel.resource); - store.add(ref); - - const model = ref.object.textEditorModel; - this._previewEditor.setModel(model); - - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - - this._elements.title.style.height = `${InlineChatFileCreatePreviewWidget.TitleHeight}px`; - const titleHightInLines = InlineChatFileCreatePreviewWidget.TitleHeight / lineHeight; - - const maxLines = Math.max(4, Math.floor((this.editor.getLayoutInfo().height / lineHeight) * .33)); - const lines = Math.min(maxLines, model.getLineCount()); - - super.show(where, titleHightInLines + lines); - } - - override hide(): void { - this._previewStore.clear(); - super.hide(); - } - - // --- layout - - protected override revealRange(range: Range, isLastLine: boolean): void { - // ignore - } - - protected override _onWidth(widthInPixel: number): void { - if (this._dim) { - this._doLayout(this._dim.height, widthInPixel); - } - } - - protected override _doLayout(heightInPixel: number, widthInPixel: number): void { - - const { lineNumbersLeft } = this.editor.getLayoutInfo(); - this._elements.title.style.marginLeft = `${lineNumbersLeft}px`; - - const newDim = new Dimension(widthInPixel, heightInPixel); - if (!Dimension.equals(this._dim, newDim)) { - this._dim = newDim; - this._previewEditor.layout(this._dim.with(undefined, this._dim.height - InlineChatFileCreatePreviewWidget.TitleHeight)); - } - } -} - - -class ButtonBarWidget { - - private readonly _domNode = h('div.buttonbar-widget'); - private readonly _buttonBar: ButtonBar; - private readonly _store = new DisposableStore(); - - constructor( - @IContextMenuService private _contextMenuService: IContextMenuService, - ) { - this._buttonBar = new ButtonBar(this.domNode); - - } - - update(allActions: IAction[][]): void { - this._buttonBar.clear(); - let secondary = false; - for (const actions of allActions) { - let btn: IButton; - const [first, ...rest] = actions; - if (!first) { - continue; - } else if (rest.length === 0) { - // single action - btn = this._buttonBar.addButton({ ...defaultButtonStyles, secondary }); - } else { - btn = this._buttonBar.addButtonWithDropdown({ - ...defaultButtonStyles, - addPrimaryActionToDropdown: false, - actions: rest, - contextMenuProvider: this._contextMenuService - }); - } - btn.label = first.label; - this._store.add(btn.onDidClick(() => first.run())); - secondary = true; - } - } - - dispose(): void { - this._buttonBar.dispose(); - this._store.dispose(); - } - - get domNode() { - return this._domNode.root; - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index a3e3865631d..175dd056ac5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -33,6 +33,8 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILogService } from 'vs/platform/log/common/log'; +import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export type TelemetryData = { @@ -67,11 +69,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 +139,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(); @@ -169,10 +165,11 @@ export class Session { readonly session: IInlineChatSession, readonly wholeRange: SessionWholeRange, readonly hunkData: HunkData, + readonly chatModel: ChatModel, ) { this.textModelNAltVersion = textModelN.getAlternativeVersionId(); this._teldata = { - extension: provider.debugName, + extension: ExtensionIdentifier.toKey(provider.extensionId), startTime: this._startTime.toISOString(), endTime: this._startTime.toISOString(), edits: 0, @@ -204,14 +201,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; } @@ -295,21 +284,11 @@ export class Session { export class SessionPrompt { - private _attempt: number = 0; - constructor( readonly value: string, + readonly attempt: number, + readonly withIntentDetection: boolean, ) { } - - get attempt() { - return this._attempt; - } - - retry() { - const result = new SessionPrompt(this.value); - result._attempt = this._attempt + 1; - return result; - } } export class SessionExchange { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index ad54eae0667..672b2be6119 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; -import { EditMode, IInlineChatSession, IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode, IInlineChatSession, IInlineChatService, IInlineChatSessionProvider, InlineChatResponseFeedbackKind, IInlineChatProgressItem, IInlineChatResponse, IInlineChatRequest } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Range } from 'vs/editor/common/core/range'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Iterable } from 'vs/base/common/iterator'; import { raceCancellation } from 'vs/base/common/async'; import { Recording, IInlineChatSessionService, ISessionKeyComputer, IInlineChatSessionEvent, IInlineChatSessionEndEvent } from './inlineChatSessionService'; -import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession'; +import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ITextModel, IValidEditOperation } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; @@ -26,6 +26,157 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; +import { IChatFollowup, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { coalesceInPlace, isNonEmptyArray } from 'vs/base/common/arrays'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { TextEdit } from 'vs/editor/common/languages'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { Codicon } from 'vs/base/common/codicons'; + +class BridgeAgent implements IChatAgentImplementation { + + constructor( + private readonly _data: IChatAgentData, + private readonly _sessions: ReadonlyMap, + @IInstantiationService private readonly _instaService: IInstantiationService, + ) { } + + + private _findSessionDataByRequest(request: IChatAgentRequest) { + let data: SessionData | undefined; + for (const candidate of this._sessions.values()) { + if (candidate.session.chatModel.sessionId === request.sessionId) { + data = candidate; + break; + } + } + return data; + } + + async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + + if (token.isCancellationRequested) { + return {}; + } + + const data = this._findSessionDataByRequest(request); + + if (!data) { + throw new Error('FAILED to find session'); + } + + const { session, editor } = data; + + if (!session.lastInput) { + throw new Error('FAILED to find last input'); + } + + const modelAltVersionIdNow = session.textModelN.getAlternativeVersionId(); + const progressEdits: TextEdit[][] = []; + + const inlineRequest: IInlineChatRequest = { + requestId: request.requestId, + prompt: request.message, + attempt: session.lastInput.attempt, + withIntentDetection: session.lastInput.withIntentDetection, + live: session.editMode !== EditMode.Preview, + previewDocument: session.textModelN.uri, + selection: editor.getSelection()!, + wholeRange: session.wholeRange.trackedInitialRange, + }; + + const inlineProgress = new Progress(data => { + // TODO@jrieken + // if (data.message) { + // progress({ kind: 'progressMessage', content: new MarkdownString(data.message) }); + // } + // TODO@ulugbekna,jrieken should we only send data.slashCommand when having detected one? + if (data.slashCommand && !inlineRequest.prompt.startsWith('/')) { + const command = this._data.slashCommands.find(c => c.name === data.slashCommand); + progress({ kind: 'agentDetection', agentId: this._data.id, command }); + } + if (data.markdownFragment) { + progress({ kind: 'markdownContent', content: new MarkdownString(data.markdownFragment) }); + } + if (isNonEmptyArray(data.edits)) { + progressEdits.push(data.edits); + progress({ kind: 'textEdit', uri: session.textModelN.uri, edits: data.edits }); + } + }); + + let result: IInlineChatResponse | undefined | null; + let response: ReplyResponse | ErrorResponse | EmptyResponse; + + try { + result = await data.session.provider.provideResponse(session.session, inlineRequest, inlineProgress, token); + + if (result) { + if (result.message) { + inlineProgress.report({ markdownFragment: result.message.value }); + } + if (Array.isArray(result.edits)) { + inlineProgress.report({ edits: result.edits }); + } + + const markdownContents = result.message ?? new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); + + response = this._instaService.createInstance(ReplyResponse, result, markdownContents, session.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId); + + } else { + response = new EmptyResponse(); + } + + } catch (e) { + response = new ErrorResponse(e); + } + + session.addExchange(new SessionExchange(session.lastInput!, response)); + + // TODO@jrieken + // result?.placeholder + // result?.wholeRange + + return { metadata: { inlineChatResponse: result } }; + } + + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + + if (!result.metadata?.inlineChatResponse) { + return []; + } + + const data = this._findSessionDataByRequest(request); + if (!data) { + return []; + } + + const inlineFollowups = await data.session.provider.provideFollowups?.(data.session.session, result.metadata?.inlineChatResponse, token); + if (!inlineFollowups) { + return []; + } + + const chatFollowups = inlineFollowups.map(f => { + if (f.kind === 'reply') { + return { + kind: 'reply', + message: f.message, + agentId: request.agentId, + title: f.title, + tooltip: f.tooltip, + } satisfies IChatFollowup; + } else { + // TODO@jrieken update API + return undefined; + } + }); + + coalesceInPlace(chatFollowups); + return chatFollowups; + } +} type SessionData = { editor: ICodeEditor; @@ -33,20 +184,30 @@ type SessionData = { store: IDisposable; }; +export class InlineChatError extends Error { + static readonly code = 'InlineChatError'; + constructor(message: string) { + super(message); + this.name = InlineChatError.code; + } +} + export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; - private readonly _onWillStartSession = new Emitter(); + private readonly _store = new DisposableStore(); + + private readonly _onWillStartSession = this._store.add(new Emitter()); readonly onWillStartSession: Event = this._onWillStartSession.event; - private readonly _onDidMoveSession = new Emitter(); + private readonly _onDidMoveSession = this._store.add(new Emitter()); readonly onDidMoveSession: Event = this._onDidMoveSession.event; - private readonly _onDidEndSession = new Emitter(); + private readonly _onDidEndSession = this._store.add(new Emitter()); readonly onDidEndSession: Event = this._onDidEndSession.event; - private readonly _onDidStashSession = new Emitter(); + private readonly _onDidStashSession = this._store.add(new Emitter()); readonly onDidStashSession: Event = this._onDidStashSession.event; private readonly _sessions = new Map(); @@ -62,15 +223,86 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @ILogService private readonly _logService: ILogService, @IInstantiationService private readonly _instaService: IInstantiationService, @IEditorService private readonly _editorService: IEditorService, - ) { } + @IChatService private readonly _chatService: IChatService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + ) { + + // MARK: register fake chat agent + + const that = this; + const agentData: IChatAgentData = { + id: 'editor', + name: 'editor', + extensionId: nullExtensionDescription.identifier, + isDefault: true, + locations: [ChatAgentLocation.Editor], + get slashCommands(): IChatAgentCommand[] { + // HACK@jrieken + // find the active session and return its slash commands + let candidate: Session | undefined; + for (const data of that._sessions.values()) { + if (data.editor.hasWidgetFocus()) { + candidate = data.session; + break; + } + } + if (!candidate || !candidate.session.slashCommands) { + return []; + } + return candidate.session.slashCommands.map(c => { + return { + name: c.command, + description: c.detail ?? '', + } satisfies IChatAgentCommand; + }); + }, + defaultImplicitVariables: [], + metadata: { + isSticky: false, + themeIcon: Codicon.copilot, + }, + }; + this._store.add(this._chatAgentService.registerDynamicAgent(agentData, this._instaService.createInstance(BridgeAgent, agentData, this._sessions))); + + // MARK: register fake chat provider + + const mapping = this._store.add(new DisposableMap()); + const registerFakeChatProvider = (provider: IInlineChatSessionProvider) => { + const d = this._chatService.registerProvider({ + id: this._asChatProviderBrigdeName(provider), + prepareSession() { + return { + id: Math.random() + }; + } + }); + mapping.set(provider, d); + }; + + this._store.add(_inlineChatService.onDidChangeProviders(e => { + if (e.added) { + registerFakeChatProvider(e.added); + } + if (e.removed) { + mapping.deleteAndDispose(e.removed); + } + })); + + for (const provider of _inlineChatService.getAllProvider()) { + registerFakeChatProvider(provider); + } + } dispose() { - this._onWillStartSession.dispose(); - this._onDidEndSession.dispose(); + this._store.dispose(); this._sessions.forEach(x => x.store.dispose()); this._sessions.clear(); } + private _asChatProviderBrigdeName(provider: IInlineChatSessionProvider) { + return `inlinechat:${provider.label}:${ExtensionIdentifier.toKey(provider.extensionId)}`; + } + async createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: Range }, token: CancellationToken): Promise { const provider = Iterable.first(this._inlineChatService.getAllProvider()); @@ -79,6 +311,19 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return undefined; } + const chatModel = this._chatService.startSession(this._asChatProviderBrigdeName(provider), token); + if (!chatModel) { + this._logService.trace('[IE] NO chatModel found'); + return undefined; + } + + const store = new DisposableStore(); + + store.add(toDisposable(() => { + this._chatService.clearSession(chatModel.sessionId); + chatModel.dispose(); + })); + this._onWillStartSession.fire(editor); const textModel = editor.getModel(); @@ -90,22 +335,38 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { token ); } catch (error) { - this._logService.error('[IE] FAILED to prepare session', provider.debugName); + this._logService.error('[IE] FAILED to prepare session', provider.extensionId); this._logService.error(error); - return undefined; + throw new InlineChatError((error as Error)?.message || 'Failed to prepare session'); } if (!rawSession) { - this._logService.trace('[IE] NO session', provider.debugName); + this._logService.trace('[IE] NO session', provider.extensionId); return undefined; } - this._logService.trace('[IE] NEW session', provider.debugName); - this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${provider.debugName}`); - const store = new DisposableStore(); + this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${provider.extensionId}`); + + + store.add(this._chatService.onDidPerformUserAction(e => { + if (e.sessionId !== chatModel.sessionId || e.action.kind !== 'vote') { + return; + } + + // TODO@jrieken VALIDATE candidate is proper, e.g check with `session.exchanges` + const request = chatModel.getRequests().find(request => request.id === e.requestId); + const candidate = request?.response?.result?.metadata?.inlineChatResponse; + if (candidate) { + provider.handleInlineChatResponseFeedback?.( + rawSession, + candidate, + e.action.direction === InteractiveSessionVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful + ); + } + })); store.add(this._inlineChatService.onDidChangeProviders(e => { if (e.removed === provider) { - this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${provider.debugName}`); + this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${provider.extensionId}`); this._releaseSession(session, true); } })); @@ -160,7 +421,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { textModelN, provider, rawSession, store.add(new SessionWholeRange(textModelN, wholeRange)), - store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)) + store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), + chatModel ); // store: key -> session @@ -191,7 +453,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { found = true; this._sessions.delete(oldKey); this._sessions.set(newKey, { ...data, editor: target }); - this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.provider.debugName}`); + this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.provider.extensionId}`); this._onDidMoveSession.fire({ session, editor: target }); break; } @@ -228,7 +490,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const [key, value] = tuple; this._sessions.delete(key); - this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.provider.debugName}`); + this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.provider.extensionId}`); this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer }); value.store.dispose(); @@ -238,7 +500,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._keepRecording(session); const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits); this._onDidStashSession.fire({ editor, session }); - this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.provider.debugName}`); + this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.provider.extensionId}`); return result; } @@ -281,5 +543,4 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { recordings(): readonly Recording[] { return this._recordings; } - } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 63088a27ed5..fa7eb2fd5e4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -28,9 +28,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { Progress } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { InlineChatFileCreatePreviewWidget, InlineChatLivePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget'; +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'; @@ -140,6 +140,7 @@ export abstract class EditModeStrategy { export class PreviewStrategy extends EditModeStrategy { private readonly _ctxDocumentChanged: IContextKey; + private readonly _previewZone: Lazy; constructor( session: Session, @@ -147,6 +148,7 @@ export class PreviewStrategy extends EditModeStrategy { zone: InlineChatZoneWidget, @IModelService modelService: IModelService, @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instaService: IInstantiationService, ) { super(session, editor, zone); @@ -158,10 +160,13 @@ export class PreviewStrategy extends EditModeStrategy { this._ctxDocumentChanged.set(session.hasChangedText); } }, undefined, this._store); + + this._previewZone = new Lazy(() => instaService.createInstance(InlineChatFileCreatePreviewWidget, editor)); } override dispose(): void { this._ctxDocumentChanged.reset(); + this._previewZone.rawValue?.dispose(); super.dispose(); } @@ -213,10 +218,10 @@ export class PreviewStrategy extends EditModeStrategy { this._zone.widget.hideEditsPreview(); } - if (response.untitledTextModel) { - this._zone.widget.showCreatePreview(response.untitledTextModel); + if (response.untitledTextModel && !response.untitledTextModel.isDisposed()) { + this._previewZone.value.showCreation(this._session.wholeRange.value.getStartPosition().delta(-1), response.untitledTextModel); } else { - this._zone.widget.hideCreatePreview(); + this._previewZone.rawValue?.hide(); } } @@ -231,166 +236,7 @@ export interface ProgressingEditsOptions { token: CancellationToken; } -export class LivePreviewStrategy extends EditModeStrategy { - - private readonly _previewZone: Lazy; - private readonly _diffZonePool: InlineChatLivePreviewWidget[] = []; - - constructor( - session: Session, - editor: ICodeEditor, - zone: InlineChatZoneWidget, - @IInstantiationService private readonly _instaService: IInstantiationService, - ) { - super(session, editor, zone); - - this._previewZone = new Lazy(() => _instaService.createInstance(InlineChatFileCreatePreviewWidget, editor)); - } - - override dispose(): void { - for (const zone of this._diffZonePool) { - zone.hide(); - zone.dispose(); - } - this._previewZone.rawValue?.hide(); - this._previewZone.rawValue?.dispose(); - super.dispose(); - } - - async apply() { - if (this._editCount > 0) { - this._editor.pushUndoStop(); - } - if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { - return; - } - const { untitledTextModel } = this._session.lastExchange.response; - if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { - await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); - } - } - - override async undoChanges(altVersionId: number): Promise { - const { textModelN } = this._session; - await undoModelUntil(textModelN, altVersionId); - this._updateDiffZones(); - } - - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise { - return this._makeChanges(edits, obs, undefined, undefined); - } - - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise { - await this._makeChanges(edits, obs, opts, new Progress(() => { - this._updateDiffZones(); - })); - } - - override async renderChanges(response: ReplyResponse): Promise { - if (response.untitledTextModel && !response.untitledTextModel.isDisposed()) { - this._previewZone.value.showCreation(this._session.wholeRange.value.getStartPosition().delta(-1), response.untitledTextModel); - } else { - this._previewZone.rawValue?.hide(); - } - - return this._updateDiffZones(); - } - - - protected _updateSummaryMessage(hunkCount: number) { - let message: string; - if (hunkCount === 0) { - message = localize('change.0', "Nothing changed"); - } else if (hunkCount === 1) { - message = localize('change.1', "1 change"); - } else { - message = localize('lines.NM', "{0} changes", hunkCount); - } - this._zone.widget.updateStatus(message); - } - - - private _updateDiffZones(): Position | undefined { - - const { hunkData } = this._session; - const hunks = hunkData.getInfo().filter(hunk => hunk.getState() === HunkState.Pending); - - if (hunks.length === 0) { - for (const zone of this._diffZonePool) { - zone.hide(); - } - - if (hunkData.getInfo().find(hunk => hunk.getState() === HunkState.Accepted)) { - this._onDidAccept.fire(); - } else { - this._onDidDiscard.fire(); - } - - return; - } - - this._updateSummaryMessage(hunks.length); - - // create enough zones - const handleDiff = () => this._updateDiffZones(); - - type Data = { position: Position; distance: number; accept: Function; discard: Function }; - let nearest: Data | undefined; - - // create enough zones - while (hunks.length > this._diffZonePool.length) { - this._diffZonePool.push(this._instaService.createInstance(InlineChatLivePreviewWidget, this._editor, this._session, {}, this._diffZonePool.length === 0 ? handleDiff : undefined)); - } - - for (let i = 0; i < hunks.length; i++) { - const hunk = hunks[i]; - this._diffZonePool[i].showForChanges(hunk); - - const modifiedRange = hunk.getRangesN()[0]; - const zoneLineNumber = this._zone.position!.lineNumber; - const distance = zoneLineNumber <= modifiedRange.startLineNumber - ? modifiedRange.startLineNumber - zoneLineNumber - : zoneLineNumber - modifiedRange.endLineNumber; - - if (!nearest || nearest.distance > distance) { - nearest = { - position: modifiedRange.getStartPosition().delta(-1), - distance, - accept: () => { - hunk.acceptChanges(); - handleDiff(); - }, - discard: () => { - hunk.discardChanges(); - handleDiff(); - } - }; - } - - } - // hide unused zones - for (let i = hunks.length; i < this._diffZonePool.length; i++) { - this._diffZonePool[i].hide(); - } - - this.acceptHunk = async () => nearest?.accept(); - this.discardHunk = async () => nearest?.discard(); - - if (nearest) { - this._zone.updatePositionAndHeight(nearest.position); - this._editor.revealPositionInCenterIfOutsideViewport(nearest.position); - } - - return nearest?.position; - } - - override hasFocus(): boolean { - return this._zone.widget.hasFocus() - || Boolean(this._previewZone.rawValue?.hasFocus()) - || this._diffZonePool.some(zone => zone.isVisible && zone.hasFocus()); - } -} type HunkDisplayData = { @@ -607,7 +453,7 @@ export class LiveStrategy extends EditModeStrategy { data.viewZoneId = undefined; } }); - this._ctxCurrentChangeShowsDiff.set(typeof data?.viewZoneId === 'number'); + this._ctxCurrentChangeShowsDiff.set(typeof data?.viewZoneId === 'string'); scrollState.restore(this._editor); }; @@ -705,7 +551,7 @@ export class LiveStrategy extends EditModeStrategy { this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position); const remainingHunks = this._session.hunkData.pending; - this._updateSummaryMessage(remainingHunks); + this._updateSummaryMessage(remainingHunks, this._session.hunkData.size); const mode = this._configService.getValue<'on' | 'off' | 'auto'>(InlineChatConfigKeys.AccessibleDiffView); @@ -741,16 +587,28 @@ export class LiveStrategy extends EditModeStrategy { return renderHunks()?.position; } - protected _updateSummaryMessage(hunkCount: number) { + private _updateSummaryMessage(remaining: number, total: number) { + + const needsReview = this._configService.getValue(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave); let message: string; - if (hunkCount === 0) { - message = localize('change.0', "Nothing changed"); - } else if (hunkCount === 1) { - message = localize('change.1', "1 change"); + if (total === 0) { + message = localize('change.0', "Nothing changed."); + } else if (remaining === 1) { + message = needsReview + ? localize('review.1', "$(info) Accept or Discard 1 change.") + : localize('change.1', "1 change"); } else { - message = localize('lines.NM', "{0} changes", hunkCount); + message = needsReview + ? localize('review.N', "$(info) Accept or Discard {0} changes.", remaining) + : localize('change.N', "{0} changes", total); } - this._zone.widget.updateStatus(message); + + let title: string | undefined; + if (needsReview) { + title = localize('review', "Review (accept or discard) all changes before continuing."); + } + + this._zone.widget.updateStatus(message, { title }); } hasFocus(): boolean { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index f65363a736b..6ebbda5d9b6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -3,40 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -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 { Dimension, getActiveElement, getTotalHeight, h, reset, trackFocus } from 'vs/base/browser/dom'; 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 { Emitter, Event } 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, 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/codeEditorWidget'; +import 'vs/css!./media/inlineChat'; +import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; -import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { EditorLayoutInfo, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; +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'; @@ -44,88 +30,32 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/br import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { FileKind } from 'vs/platform/files/common/files'; 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 { 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 { asCssVariable, asCssVariableName, editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; 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 { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +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 { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; - -const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand, inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { chatRequestBackground } from 'vs/workbench/contrib/chat/common/chatColors'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { isNonEmptyArray, tail } from 'vs/base/common/arrays'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -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 -}; - -const _previewEditorEditorOptions: IDiffEditorConstructionOptions = { - scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, - renderMarginRevertIcon: false, - diffCodeLens: false, - scrollBeyondLastLine: false, - stickyScroll: { enabled: false }, - originalAriaLabel: localize('original', 'Original'), - modifiedAriaLabel: localize('modified', 'Modified'), - diffAlgorithm: 'advanced', - readOnly: true, - isInEmbeddedEditor: true -}; export interface InlineChatWidgetViewState { editorViewState: ICodeEditorViewState; @@ -134,10 +64,36 @@ 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; + + /** + * @deprecated + * TODO@meganrogge,jrieken + * We need a way to make this configurable per editor/resource and not + * globally. + */ + editableCodeBlocks?: boolean; + + editorOverflowWidgetsDomNode?: HTMLElement; } export interface IInlineChatMessage { @@ -154,31 +110,13 @@ 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.widget-toolbar@widgetToolbar') - ]), + h('div.chat-widget@chatWidget'), h('div.progress@progress'), - h('div.detectedIntent.hidden@detectedIntent'), - h('div.previewDiff.hidden@previewDiff'), - h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), - h('div.previewCreate.hidden@previewCreate'), - h('div.chatMessage.hidden@chatMessage', [ - h('div.chatMessageContent@chatMessageContent'), - h('div.messageActions@messageActions') - ]), h('div.followUps.hidden@followUps'), + h('div.previewDiff.hidden@previewDiff'), h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ h('div.label.info.hidden@infoLabel'), @@ -189,219 +127,157 @@ 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; + protected readonly _store = new DisposableStore(); + + private readonly _defaultChatModel: ChatModel; private readonly _ctxInputEditorFocused: IContextKey; private readonly _ctxResponseFocused: IContextKey; private readonly _progressBar: ProgressBar; + private readonly _chatWidget: ChatWidget; - private readonly _previewDiffEditor: Lazy; - private readonly _previewDiffModel = this._store.add(new MutableDisposable()); - - private readonly _accessibleViewer = this._store.add(new MutableDisposable()); - - private readonly _previewCreateTitle: ResourceLabel; - private readonly _previewCreateEditor: Lazy; - private readonly _previewCreateDispoable = this._store.add(new MutableDisposable()); - - private readonly _onDidChangeHeight = this._store.add(new MicrotaskEmitter()); + protected readonly _onDidChangeHeight = this._store.add(new Emitter()); readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); - private readonly _onDidChangeLayout = this._store.add(new MicrotaskEmitter()); private readonly _onDidChangeInput = this._store.add(new Emitter()); readonly onDidChangeInput: Event = this._onDidChangeInput.event; - private readonly _onRequestWithoutIntentDetection = this._store.add(new Emitter()); - readonly onRequestWithoutIntentDetection: Event = this._onRequestWithoutIntentDetection.event; - - private _lastDim: Dimension | undefined; private _isLayouting: boolean = false; - private _preferredExpansionState: ExpansionState | undefined; - private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED; - private _slashCommandDetails: { command: string; detail: string }[] = []; - private _slashCommandContentWidget: SlashCommandContentWidget; - - private readonly _editorOptions: ChatEditorOptions; - private _chatMessageDisposables = this._store.add(new DisposableStore()); private _followUpDisposables = this._store.add(new DisposableStore()); - private _slashCommandUsedDisposables = this._store.add(new DisposableStore()); - - private _chatMessage: MarkdownString | undefined; - constructor( - private readonly parentEditor: ICodeEditor, - _options: IInlineChatWidgetConstructionOptions, - @IModelService private readonly _modelService: IModelService, + location: ChatAgentLocation, + 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, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @ITextModelService protected readonly _textModelResolverService: ITextModelService, + @IChatService private readonly _chatService: IChatService, ) { - - // 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())); + // Share hover delegates between toolbars to support instant hover between both + // TODO@jrieken move into chat widget + // const hoverDelegate = this._store.add(createInstantHoverDelegate()); this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { this._updateAriaLabel(); + // TODO@jrieken FIX THIS + // this._chatWidget.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); } })); - 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)); + // toolbars + this._progressBar = new ProgressBar(this._elements.progress); + this._store.add(this._progressBar); + let allowRequests = false; - // --- context keys - this._ctxMessageCropState = CTX_INLINE_CHAT_MESSAGE_CROP_STATE.bindTo(this._contextKeyService); - this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(this._contextKeyService); + const scopedInstaService = _instantiationService.createChild( + new ServiceCollection([ + IContextKeyService, + this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)) + ]) + ); - 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); + this._chatWidget = scopedInstaService.createInstance( + ChatWidget, + location, + { resource: true }, + { + defaultElementHeight: 32, + renderStyle: 'compact', + renderInputOnTop: true, + supportsFileReferences: true, + editorOverflowWidgetsDomNode: options.editorOverflowWidgetsDomNode, + editableCodeBlocks: options.editableCodeBlocks, + menus: { + executeToolbar: options.inputMenuId, + inputSideToolbar: options.widgetMenuId, + telemetrySource: options.telemetrySource + }, + filter: item => { + if (isWelcomeVM(item)) { + return false; + } + if (isRequestVM(item)) { + return allowRequests; + } + return true; + }, + }, + { + listForeground: editorForeground, + listBackground: inlineChatBackground, + inputEditorBackground: inputBackground, + resultEditorBackground: editorBackground + } + ); + this._chatWidget.render(this._elements.chatWidget); + this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground)); + this._chatWidget.setVisible(true); + this._store.add(this._chatWidget); + + const viewModelListener = this._store.add(new MutableDisposable()); + this._store.add(this._chatWidget.onDidChangeViewModel(() => { + const model = this._chatWidget.viewModel; + + if (!model) { + allowRequests = false; + viewModelListener.clear(); + return; + } - // (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; + const updateAllowRequestsFilter = () => { + let requestCount = 0; + for (const item of model.getItems()) { + if (isRequestVM(item)) { + if (++requestCount >= 2) { + break; + } + } } - if (selectionTop === lastViewLineTop) { - onLast = true; + const newAllowRequest = requestCount >= 2; + if (newAllowRequest !== allowRequests) { + allowRequests = newAllowRequest; + this._chatWidget.refilter(); } - } - 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(); + }; + viewModelListener.value = model.onDidChange(updateAllowRequestsFilter); })); - 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(); + const viewModelStore = this._store.add(new DisposableStore()); + this._store.add(this._chatWidget.onDidChangeViewModel(() => { + viewModelStore.clear(); + const viewModel = this._chatWidget.viewModel; + if (viewModel) { + viewModelStore.add(viewModel.onDidChange(() => 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); - - // toolbars + this._onDidChangeHeight.fire(); + })); - 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 + this._store.add(this.chatWidget.onDidChangeContentHeight(() => { + this._onDidChangeHeight.fire(); })); - this._progressBar = new ProgressBar(this._elements.progress); - this._store.add(this._progressBar); + // context keys + this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); + const tracker = this._store.add(trackFocus(this.domNode)); + this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false))); + this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true))); + this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService); + this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true))); + this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false))); - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', - toolbarOptions: { primaryGroup: 'main' } - })); + const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; - 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 statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); @@ -414,38 +290,33 @@ 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._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + // this._elements.status + this._store.add(setupCustomHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { + return this._elements.statusLabel.dataset['title']; + })); + + this._store.add(this._chatService.onDidPerformUserAction(e => { + if (e.sessionId === this._chatWidget.viewModel?.model.sessionId && e.action.kind === 'vote') { + this.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); } })); - } + // LEGACY - default chat model + // this is only here for as long as we offer updateChatMessage + this._defaultChatModel = this._store.add(this._instantiationService.createInstance(ChatModel, `inlineChatDefaultModel/${location}`, undefined)); + this._defaultChatModel.startInitialize(); + this._defaultChatModel.initialize({ id: 1 }, undefined); + this.setChatModel(this._defaultChatModel); + } private _updateAriaLabel(): void { if (!this._accessibilityService.isScreenReaderOptimized()) { @@ -456,72 +327,78 @@ 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 }); + this._chatWidget.inputEditor.updateOptions({ ariaLabel: label }); } dispose(): void { this._store.dispose(); - this._ctxInputEmpty.reset(); - this._ctxMessageCropState.reset(); } get domNode(): HTMLElement { return this._elements.root; } - layout(_dim: 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`; - } + get chatWidget(): ChatWidget { + return this._chatWidget; + } + saveState() { + this._chatWidget.saveState(); + } - 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(); - } + layout(widgetDim: Dimension) { + this._isLayouting = true; + try { + this._doLayout(widgetDim); } finally { this._isLayouting = false; } } - getHeight(): number { - const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status); - const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; - const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent); + protected _doLayout(dimension: Dimension): void { + const extraHeight = this._getExtraHeight(); + const progressHeight = getTotalHeight(this._elements.progress); 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); + + // console.log('ZONE#Widget#layout', { height: dimension.height, extraHeight, progressHeight, followUpsHeight, statusHeight, LIST: dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight }); + + this._elements.root.style.height = `${dimension.height - extraHeight}px`; + this._elements.root.style.width = `${dimension.width}px`; + this._elements.progress.style.width = `${dimension.width}px`; + + this._chatWidget.layout( + dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight, + dimension.width + ); + } + + /** + * The content height of this widget is the size that would require no scrolling + */ + get contentHeight(): number { + const data = { + followUpsHeight: getTotalHeight(this._elements.followUps), + chatWidgetContentHeight: this._chatWidget.contentHeight, + progressHeight: getTotalHeight(this._elements.progress), + statusHeight: getTotalHeight(this._elements.status), + extraHeight: this._getExtraHeight() + }; + const result = data.progressHeight + data.chatWidgetContentHeight + data.followUpsHeight + data.statusHeight + data.extraHeight; + return result; + } + + get minHeight(): number { + // The chat widget is variable height and supports scrolling. It + // should be at least 100px high and at most the content height. + let value = this.contentHeight; + value -= this._chatWidget.contentHeight; + value += Math.min(100, this._chatWidget.contentHeight); + return value; + } + + protected _getExtraHeight(): number { + return 12 /* padding */ + 2 /*border*/ + 12 /*shadow*/; } updateProgress(show: boolean) { @@ -535,38 +412,29 @@ export class InlineChatWidget { } get value(): string { - return this._inputModel.getValue(); + return this._chatWidget.getInput(); } set value(value: string) { - this._inputModel.setValue(value); - this._inputEditor.setPosition(this._inputModel.getFullModelRange().getEndPosition()); + this._chatWidget.setInput(value); } - selectAll(includeSlashCommand: boolean = true) { - let selection = this._inputModel.getFullModelRange(); + selectAll(includeSlashCommand: boolean = true) { + // DEBT@jrieken + // REMOVE when agents are adopted + let startColumn = 1; 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; + const match = /^(\/\w+)\s*/.exec(this._chatWidget.inputEditor.getModel()!.getLineContent(1)); + if (match) { + startColumn = match[1].length + 1; + } } - - this._inputEditor.setSelection(selection); + this._chatWidget.inputEditor.setSelection(new Selection(1, startColumn, Number.MAX_SAFE_INTEGER, 1)); } 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); - } + this._chatWidget.setInputPlaceholder(value); } updateToolbar(show: boolean) { @@ -577,69 +445,86 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - get expansionState(): ExpansionState { - return this._expansionState; + async getCodeBlockInfo(codeBlockIndex: number): Promise { + const { viewModel } = this._chatWidget; + if (!viewModel) { + return undefined; + } + for (const item of viewModel.getItems()) { + if (isResponseVM(item)) { + return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model; + } + } + return undefined; } - set preferredExpansionState(expansionState: ExpansionState | undefined) { - this._preferredExpansionState = expansionState; + get responseContent(): string | undefined { + const requests = this._chatWidget.viewModel?.model.getRequests(); + if (!isNonEmptyArray(requests)) { + return undefined; + } + return tail(requests).response?.response.asString(); } - get responseContent(): string | undefined { - return this._chatMessage?.value; + getChatModel(): IChatModel { + return this._chatWidget.viewModel?.model ?? this._defaultChatModel; } + setChatModel(chatModel: IChatModel) { + this._chatWidget.setModel(chatModel, { inputValue: undefined }); + } + + + /** + * @deprecated use `setChatModel` instead + */ + addToHistory(input: string) { + if (this._chatWidget.viewModel?.model === this._defaultChatModel) { + this._chatWidget.input.acceptInput(input); + } + } + + /** + * @deprecated use `setChatModel` instead + */ updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; updateChatMessage(message: IInlineChatMessage | undefined): void; - updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined { - let expansionState: ExpansionState; - this._chatMessageDisposables.clear(); - this._chatMessage = message ? new MarkdownString(message.message.value) : undefined; - const hasMessage = message?.message.value; - this._elements.chatMessage.classList.toggle('hidden', !hasMessage); - reset(this._elements.chatMessageContent); - 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)); - const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); - const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); - const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; - const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; - const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, undefined)); - renderer.layout(this._elements.chatMessageContent.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); - })); - const template = renderer.renderTemplate(this._elements.chatMessageContent); - this._chatMessageDisposables.add(template.elementDisposables); - this._chatMessageDisposables.add(template.templateDisposables); - renderer.renderChatTreeItem(viewModel, 0, template); - this._chatMessageDisposables.add(renderer.onDidChangeItemHeight(() => this._onDidChangeHeight.fire())); - - if (this._preferredExpansionState) { - expansionState = this._preferredExpansionState; - this._preferredExpansionState = undefined; - } else { - this._updateLineClamp(ExpansionState.CROPPED); - expansionState = template.value.scrollHeight > template.value.clientHeight ? ExpansionState.CROPPED : ExpansionState.NOT_CROPPED; + updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined; + updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined { + + if (!this._chatWidget.viewModel || this._chatWidget.viewModel.model !== this._defaultChatModel) { + // this can only be used with the default chat model + return; + } + + const model = this._defaultChatModel; + if (!message?.message.value) { + for (const request of model.getRequests()) { + model.removeRequest(request.id); } - 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); - } - } : undefined; + return; } - this._expansionState = expansionState; - this._onDidChangeHeight.fire(); - return resultingAppender; + + const chatRequest = model.addRequest({ parts: [], text: '' }, { variables: [] }); + model.acceptResponseProgress(chatRequest, { + kind: 'markdownContent', + content: message.message + }); + + if (!isIncomplete) { + model.completeResponse(chatRequest); + return; + } + return { + cancel: () => model.cancelRequest(chatRequest), + complete: () => model.completeResponse(chatRequest), + appendContent: (fragment: string) => { + model.acceptResponseProgress(chatRequest, { + kind: 'markdownContent', + content: new MarkdownString(fragment) + }); + } + }; } updateFollowUps(items: IInlineChatFollowup[], onFollowup: (followup: IInlineChatFollowup) => void): void; @@ -650,55 +535,15 @@ export class InlineChatWidget { reset(this._elements.followUps); if (items && items.length > 0 && onFollowup) { this._followUpDisposables.add( - this._instantiationService.createInstance(ChatFollowups, this._elements.followUps, items, undefined, onFollowup)); + this._instantiationService.createInstance(ChatFollowups, this._elements.followUps, items, ChatAgentLocation.Editor, undefined, onFollowup)); } this._onDidChangeHeight.fire(); } - updateChatMessageExpansionState(expansionState: ExpansionState) { - this._ctxMessageCropState.set(expansionState); - const heightBefore = this._elements.chatMessageContent.scrollHeight; - this._updateLineClamp(expansionState); - const heightAfter = this._elements.chatMessageContent.scrollHeight; - if (heightBefore === heightAfter) { - this._ctxMessageCropState.set(ExpansionState.NOT_CROPPED); - } - this._onDidChangeHeight.fire(); - } - - private _updateLineClamp(expansionState: ExpansionState) { - this._elements.chatMessageContent.setAttribute('state', expansionState); - } - updateSlashCommandUsed(command: string): void { - const details = this._slashCommandDetails.find(candidate => candidate.command === command); - if (!details) { - return; - } - - this._elements.detectedIntent.classList.toggle('hidden', false); - - this._slashCommandUsedDisposables.clear(); - - const label = localize('slashCommandUsed', "Using {0} to generate response ([[re-run without]])", `\`\`/${details.command}\`\``); - const usingSlashCommandText = renderFormattedText(label, { - inline: true, - renderCodeSegments: true, - className: 'slash-command-pill', - actionHandler: { - callback: (content) => { - if (content !== '0') { - return; - } - this._elements.detectedIntent.classList.toggle('hidden', true); - this._onRequestWithoutIntentDetection.fire(); - }, - disposables: this._slashCommandUsedDisposables, - } - }); - - reset(this._elements.detectedIntent, usingSlashCommandText); - this._onDidChangeHeight.fire(); + updateSlashCommands(commands: IInlineChatSlashCommand[]) { + // this._inputWidget.updateSlashCommands(commands); + // TODO@jrieken } updateInfo(message: string): void { @@ -708,16 +553,18 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - updateStatus(message: string, ops: { classes?: string[]; resetAfter?: number; keepMessage?: boolean } = {}) { + updateStatus(message: string, ops: { classes?: string[]; resetAfter?: number; keepMessage?: boolean; title?: string } = {}) { const isTempMessage = typeof ops.resetAfter === 'number'; if (isTempMessage && !this._elements.statusLabel.dataset['state']) { const statusLabel = this._elements.statusLabel.innerText; + const title = this._elements.statusLabel.dataset['title']; const classes = Array.from(this._elements.statusLabel.classList.values()); setTimeout(() => { - this.updateStatus(statusLabel, { classes, keepMessage: true }); + this.updateStatus(statusLabel, { classes, keepMessage: true, title }); }, ops.resetAfter); } - reset(this._elements.statusLabel, message); + const renderedMessage = renderLabelWithIcons(message); + reset(this._elements.statusLabel, ...renderedMessage); this._elements.statusLabel.className = `label status ${(ops.classes ?? []).join(' ')}`; this._elements.statusLabel.classList.toggle('hidden', !message); if (isTempMessage) { @@ -725,205 +572,135 @@ export class InlineChatWidget { } else { delete this._elements.statusLabel.dataset['state']; } + + if (ops.title) { + this._elements.statusLabel.dataset['title'] = ops.title; + } else { + delete this._elements.statusLabel.dataset['title']; + } this._onDidChangeHeight.fire(); } reset() { - this._ctxInputEmpty.reset(); - this._ctxInnerCursorFirst.reset(); - this._ctxInnerCursorLast.reset(); - this._ctxInputEditorFocused.reset(); - - this.value = ''; + this._chatWidget.saveState(); this.updateChatMessage(undefined); this.updateFollowUps(undefined); reset(this._elements.statusLabel); - this._elements.detectedIntent.classList.toggle('hidden', true); this._elements.statusLabel.classList.toggle('hidden', true); 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._chatWidget.focusInput(); } hasFocus() { return this.domNode.contains(getActiveElement()); } - // --- preview - - showEditsPreview(hunks: HunkData, textModel0: ITextModel, textModelN: ITextModel) { - - if (hunks.size === 0) { - this.hideEditsPreview(); - return; - } - - this._elements.previewDiff.classList.remove('hidden'); - - this._previewDiffEditor.value.setModel({ original: textModel0, modified: textModelN }); +} - // joined ranges - let originalLineRange: LineRange | undefined; - let modifiedLineRange: LineRange | undefined; - for (const item of hunks.getInfo()) { - const [first0] = item.getRanges0(); - const [firstN] = item.getRangesN(); +const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); - originalLineRange = !originalLineRange ? LineRange.fromRangeInclusive(first0) : originalLineRange.join(LineRange.fromRangeInclusive(first0)); - modifiedLineRange = !modifiedLineRange ? LineRange.fromRangeInclusive(firstN) : modifiedLineRange.join(LineRange.fromRangeInclusive(firstN)); - } +const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SnippetController2.ID, + SuggestController.ID + ]) +}; - if (!originalLineRange || !modifiedLineRange) { - this.hideEditsPreview(); - return; - } +const _previewEditorEditorOptions: IDiffEditorConstructionOptions = { + scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, + renderMarginRevertIcon: false, + diffCodeLens: false, + scrollBeyondLastLine: false, + stickyScroll: { enabled: false }, + originalAriaLabel: localize('original', 'Original'), + modifiedAriaLabel: localize('modified', 'Modified'), + diffAlgorithm: 'advanced', + readOnly: true, + isInEmbeddedEditor: true +}; - const hiddenOriginal = invertLineRange(originalLineRange, textModel0); - const hiddenModified = invertLineRange(modifiedLineRange, textModelN); - this._previewDiffEditor.value.getOriginalEditor().setHiddenAreas(hiddenOriginal.map(lr => asRange(lr, textModel0)), 'diff-hidden'); - this._previewDiffEditor.value.getModifiedEditor().setHiddenAreas(hiddenModified.map(lr => asRange(lr, textModelN)), 'diff-hidden'); - this._previewDiffEditor.value.revealLine(modifiedLineRange.startLineNumber, ScrollType.Immediate); - this._onDidChangeHeight.fire(); - } +export class EditorBasedInlineChatWidget extends InlineChatWidget { - hideEditsPreview() { - this._elements.previewDiff.classList.add('hidden'); - if (this._previewDiffEditor.hasValue) { - this._previewDiffEditor.value.setModel(null); - } - this._previewDiffModel.clear(); - this._onDidChangeHeight.fire(); - } + private readonly _accessibleViewer = this._store.add(new MutableDisposable()); - async showCreatePreview(model: IUntitledTextEditorModel): Promise { - this._elements.previewCreateTitle.classList.remove('hidden'); - this._elements.previewCreate.classList.remove('hidden'); + private readonly _previewDiffEditor: Lazy; + private readonly _previewDiffModel = this._store.add(new MutableDisposable()); - const ref = await this._textModelResolverService.createModelReference(model.resource); - this._previewCreateDispoable.value = ref; - this._previewCreateTitle.element.setFile(model.resource, { fileKind: FileKind.FILE }); + 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, + @ITextModelService textModelResolverService: ITextModelService, + @IChatService chatService: IChatService, + ) { + super(ChatAgentLocation.Editor, { ...options, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService); - this._previewCreateEditor.value.setModel(ref.object.textEditorModel); - this._onDidChangeHeight.fire(); + // 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))); } - hideCreatePreview() { - this._elements.previewCreateTitle.classList.add('hidden'); - this._elements.previewCreate.classList.add('hidden'); - this._previewCreateEditor.rawValue?.setModel(null); - this._previewCreateDispoable.clear(); - this._previewCreateTitle.element.clear(); - this._onDidChangeHeight.fire(); - } + // --- layout - showsAnyPreview() { - return !this._elements.previewDiff.classList.contains('hidden') || - !this._elements.previewCreate.classList.contains('hidden'); + override get contentHeight(): number { + let result = super.contentHeight; + if (this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel()) { + result += 14 + Math.min(300, this._previewDiffEditor.value.getContentHeight()); + } + if (this._accessibleViewer.value) { + result += this._accessibleViewer.value.height; + } + return result; } - // --- slash commands + protected override _doLayout(dimension: Dimension): void { - updateSlashCommands(commands: IInlineChatSlashCommand[]) { + let newHeight = dimension.height; - this._slashCommands.clear(); - if (commands.length === 0) { - return; + if (this._previewDiffEditor.hasValue) { + const previewDiffDim = new Dimension(dimension.width - 12, Math.min(300, 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); + newHeight -= previewDiffDim.height + 14; } - 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}`; + if (this._accessibleViewer.value) { + this._accessibleViewer.value.width = dimension.width - 12; + newHeight -= this._accessibleViewer.value.height; + } - 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 - }; - }); + super._doLayout(dimension.with(undefined, newHeight)); - 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(); + // update/fix the height of the zone which was set to newHeight in super._doLayout + this._elements.root.style.height = `${dimension.height - this._getExtraHeight()}px`; } + override reset() { + this.hideEditsPreview(); + this._accessibleViewer.clear(); + super.reset(); + } // --- accessible viewer @@ -936,165 +713,62 @@ export class InlineChatWidget { this._elements.accessibleViewer, session, hunkData, - new AccessibleHunk(this.parentEditor, 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)); - } + // --- preview - private _computeHeightInLines(): number { - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - return this.widget.getHeight() / lineHeight; - } + showEditsPreview(hunks: HunkData, textModel0: ITextModel, textModelN: ITextModel) { - protected override _relayout() { - if (this._dimension) { - this._doLayout(this._dimension.height); + if (hunks.size === 0) { + this.hideEditsPreview(); + return; } - super._relayout(this._computeHeightInLines()); - } - override show(position: Position): void { - super.show(position, this._computeHeightInLines()); - this.widget.focus(); - this._ctxVisible.set(true); - } + this._elements.previewDiff.classList.remove('hidden'); - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; - } + this._previewDiffEditor.value.setModel({ original: textModel0, modified: textModelN }); - updateBackgroundColor(newPosition: Position, wholeRange: IRange) { - assertType(this.container); - const widgetLineNumber = newPosition.lineNumber; - this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); - } + // joined ranges + let originalLineRange: LineRange | undefined; + let modifiedLineRange: LineRange | undefined; + for (const item of hunks.getInfo()) { + const [first0] = item.getRanges0(); + const [firstN] = item.getRangesN(); - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; + originalLineRange = !originalLineRange ? LineRange.fromRangeInclusive(first0) : originalLineRange.join(LineRange.fromRangeInclusive(first0)); + modifiedLineRange = !modifiedLineRange ? LineRange.fromRangeInclusive(firstN) : modifiedLineRange.join(LineRange.fromRangeInclusive(firstN)); } - 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; - } + + if (!originalLineRange || !modifiedLineRange) { + this.hideEditsPreview(); + return; } - return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); - } - setContainerMargins(): void { - assertType(this.container); + const hiddenOriginal = invertLineRange(originalLineRange, textModel0); + const hiddenModified = invertLineRange(modifiedLineRange, textModelN); + this._previewDiffEditor.value.getOriginalEditor().setHiddenAreas(hiddenOriginal.map(lr => asRange(lr, textModel0)), 'diff-hidden'); + this._previewDiffEditor.value.getModifiedEditor().setHiddenAreas(hiddenModified.map(lr => asRange(lr, textModelN)), 'diff-hidden'); + this._previewDiffEditor.value.revealLine(modifiedLineRange.startLineNumber, ScrollType.Immediate); - const info = this.editor.getLayoutInfo(); - const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; - this.container.style.marginLeft = `${marginWithoutIndentation}px`; + this._onDidChangeHeight.fire(); } - setWidgetMargins(position: Position): void { - const indentationWidth = this._calculateIndentationWidth(position); - if (this._indentationWidth === indentationWidth) { - return; + hideEditsPreview() { + this._elements.previewDiff.classList.add('hidden'); + if (this._previewDiffEditor.hasValue) { + this._previewDiffEditor.value.setModel(null); } - 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`; + this._previewDiffModel.clear(); + this._onDidChangeHeight.fire(); } - 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')); + showsAnyPreview() { + return !this._elements.previewDiff.classList.contains('hidden'); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts new file mode 100644 index 00000000000..e1c130a3b8b --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * 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_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, 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'; +import { MenuId } from 'vs/platform/actions/common/actions'; + + +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: MenuId.ChatExecute, + 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 || action.id === ACTION_TOGGLE_DIFF) { + 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(() => { + if (this.position) { + // only relayout when visible + this._relayout(this._computeHeightInLines()); + } + })); + 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 chatContentHeight = this.widget.contentHeight; + const editorHeight = this.editor.getLayoutInfo().height; + + const contentHeight = Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42)); + const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight); + return heightInLines; + } + + protected override _onWidth(_widthInPixel: number): void { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + } + + 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`; + + super.show(position, this._computeHeightInLines()); + this._setWidgetMargins(position); + this.widget.focus(); + } + + override updatePositionAndHeight(position: Position): void { + super.updatePositionAndHeight(position, this._computeHeightInLines()); + this._setWidgetMargins(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(); + if (!visibleRange.containsPosition(position)) { + // this is needed because `getOffsetForColumn` won't work when the position + // isn't visible/rendered + return 0; + } + + let indentationLevel = viewModel.getLineFirstNonWhitespaceColumn(position.lineNumber); + let indentationLineNumber = position.lineNumber; + for (let lineNumber = position.lineNumber; lineNumber >= visibleRange.startLineNumber; lineNumber--) { + const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); + if (currentIndentationLevel !== 0) { + indentationLineNumber = lineNumber; + indentationLevel = currentIndentationLevel; + break; + } + } + + return Math.max(0, this.editor.getOffsetForColumn(indentationLineNumber, indentationLevel)); // double-guard against invalie getOffsetForColumn-calls + } + + 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/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css new file mode 100644 index 00000000000..15edf2661e2 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -0,0 +1,304 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .zone-widget.inline-chat-widget { + z-index: 3; +} + +.monaco-workbench .zone-widget.inline-chat-widget .interactive-session { + max-width: unset; +} + +.monaco-workbench .zone-widget-container.inside-selection { + background-color: var(--vscode-inlineChat-regionHighlight); +} + +.monaco-workbench .inline-chat { + color: inherit; + padding: 6px; + border-radius: 6px; + border: 1px solid var(--vscode-inlineChat-border); + box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); + background: var(--vscode-inlineChat-background); +} + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { + margin-bottom: 1px; +} + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-input-and-execute-toolbar { + width: 100%; +} + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact { + padding: 4px +} + +/* progress bit */ + +.monaco-workbench .inline-chat .progress { + position: relative; +} + +/* UGLY - fighting against workbench styles */ +.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { + top: 0; +} + +/* status */ + +.monaco-workbench .inline-chat .status { + display: flex; + justify-content: space-between; + align-items: center; +} + + +.monaco-workbench .inline-chat .status .actions.hidden { + display: none; +} + +.monaco-workbench .inline-chat .status .label { + overflow: hidden; + color: var(--vscode-descriptionForeground); + font-size: 11px; + padding-top: 4px; + padding-left: 10px; + display: inline-flex; +} + +.monaco-workbench .inline-chat .status .label.info { + margin-right: auto; + padding-left: 2px; +} + +.monaco-workbench .inline-chat .status .label.status { + margin-left: auto; +} + +.monaco-workbench .inline-chat .status .label.hidden { + display: none; +} + +.monaco-workbench .inline-chat .status .label.error { + color: var(--vscode-errorForeground); +} + +.monaco-workbench .inline-chat .status .label.warn { + color: var(--vscode-editorWarning-foreground); +} + +.monaco-workbench .inline-chat .status .label > .codicon { + padding: 0 5px; + font-size: 12px; + line-height: 18px; +} + +.monaco-workbench .inline-chat .chatMessage .chatMessageContent .value { + overflow: hidden; + -webkit-user-select: text; + user-select: text; +} + +.monaco-workbench .inline-chat .followUps { + padding: 5px 5px; +} + +.monaco-workbench .inline-chat .followUps .interactive-session-followups .monaco-button { + display: block; + color: var(--vscode-textLink-foreground); + font-size: 12px; +} + +.monaco-workbench .inline-chat .followUps.hidden { + display: none; +} + +.monaco-workbench .inline-chat .chatMessage { + padding: 0 3px; +} + +.monaco-workbench .inline-chat .chatMessage .chatMessageContent { + padding: 2px 2px; +} + +.monaco-workbench .inline-chat .chatMessage.hidden { + display: none; +} + +.monaco-workbench .inline-chat .status .actions { + display: flex; + padding-top: 4px; +} + +.monaco-workbench .inline-chat .status .actions > .monaco-button, +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown { + margin-right: 6px; +} + +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { + display: flex; + align-items: center; + padding: 0 4px; +} + +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon { + display: flex; +} + +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon::before { + align-self: center; +} + +.monaco-workbench .inline-chat .status .actions .monaco-text-button { + padding: 2px 4px; + white-space: nowrap; +} + +.monaco-workbench .inline-chat .status .monaco-toolbar .action-item { + padding: 0 2px; +} + +/* TODO@jrieken not needed? */ +.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-workbench .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { + background-color: var(--vscode-button-hoverBackground); +} + +/* preview */ + +.monaco-workbench .inline-chat .preview { + display: none; +} + +.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-workbench .inline-chat .previewCreateTitle { + padding-top: 6px; +} + +.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-workbench .inline-chat-toolbar { + display: flex; +} + +.monaco-workbench .inline-chat-toolbar > .monaco-button { + margin-right: 6px; +} + +.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); +} + +/* decoration styles */ + +.monaco-workbench .inline-chat-inserted-range { + background-color: var(--vscode-inlineChatDiff-inserted); +} + +.monaco-workbench .inline-chat-inserted-range-linehighlight { + background-color: var(--vscode-diffEditor-insertedLineBackground); +} + +.monaco-workbench .inline-chat-original-zone2 { + background-color: var(--vscode-diffEditor-removedLineBackground); + opacity: 0.8; +} + +.monaco-workbench .inline-chat-lines-inserted-range { + background-color: var(--vscode-diffEditor-insertedTextBackground); +} + +.monaco-workbench .inline-chat-block-selection { + background-color: var(--vscode-inlineChat-regionHighlight); +} + +.monaco-workbench .interactive-session .interactive-input-and-execute-toolbar .monaco-editor .inline-chat-slash-command { + background-color: var(--vscode-chat-slashCommandBackground); + color: var(--vscode-chat-slashCommandForeground); /* Overrides the foreground color rule in chat.css */ + border-radius: 4px; + padding: 1px; +} + +.monaco-workbench .inline-chat-slash-command-detail { + opacity: 0.5; +} + +/* diff zone */ + +.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-workbench .inline-chat-newfile-widget { + background-color: var(--vscode-inlineChat-regionHighlight); +} + +.monaco-workbench .inline-chat-newfile-widget .title { + display: flex; + align-items: center; + justify-content: space-between; +} + +.monaco-workbench .inline-chat-newfile-widget .title .detail { + margin-left: 4px; +} + +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget { + display: flex; + margin-left: auto; + margin-right: 8px; +} + +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { + display: inline-flex; + white-space: nowrap; + margin-left: 4px; +} + +/* gutter decoration */ + +.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-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { + opacity: 0.5; +} + +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { + opacity: 0; +} + +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { + opacity: 1; +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css new file mode 100644 index 00000000000..5ea5b13496c --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .inline-chat-content-widget { + z-index: 50; + 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.interactive-session .interactive-session { + max-width: unset; +} + +.monaco-workbench .inline-chat-content-widget.interactive-session .interactive-input-part .interactive-execute-toolbar { + margin-bottom: 1px; +} + +.monaco-workbench .inline-chat-content-widget.interactive-session .interactive-input-part.compact { + padding: 0; +} + +.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/src/vs/workbench/contrib/inlineChat/browser/utils.ts b/src/vs/workbench/contrib/inlineChat/browser/utils.ts index fbd21af7b65..ef84a63f4e9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/utils.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/utils.ts @@ -77,7 +77,7 @@ export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdi export function asProgressiveEdit(interval: IntervalTimer, edit: IIdentifiedSingleEditOperation, wordsPerSec: number, token: CancellationToken): AsyncTextEdit { - wordsPerSec = Math.max(10, wordsPerSec); + wordsPerSec = Math.max(30, wordsPerSec); const stream = new AsyncIterableSource(); let newText = edit.text ?? ''; diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index dbf95c3bb0b..e1716bcc0da 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -21,6 +21,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBackground, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ExtensionsMigration, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export interface IInlineChatSlashCommand { command: string; @@ -116,7 +117,7 @@ export type IInlineChatFollowup = IInlineChatReplyFollowup | IInlineChatCommandF export interface IInlineChatSessionProvider { - debugName: string; + extensionId: ExtensionIdentifier; label: string; supportIssueReporting?: boolean; @@ -156,7 +157,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")); @@ -176,12 +176,11 @@ export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inl export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; +export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; // --- menus -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'); @@ -209,9 +208,7 @@ export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewR export const enum EditMode { Live = 'live', - Preview = 'preview', - /** @deprecated */ - LivePreview = 'livePreview', + Preview = 'preview' } Registry.as(ExtensionsMigration.ConfigurationMigration).registerConfigurationMigrations( diff --git a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts index b75bb758c6c..7f24f989723 100644 --- a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts +++ b/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/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 932cb5056b1..cf72ac94a38 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -8,15 +8,17 @@ import { equals } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { mock } from 'vs/base/test/common/mock'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { TestDiffProviderFactoryService } from 'vs/editor/browser/diff/testDiffProviderFactoryService'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; +import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService'; import { instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -29,27 +31,41 @@ import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/co import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; -import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; -import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatEditResponse, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; +import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; +import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; 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 { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { TestContextService, TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; suite('InteractiveChatController', function () { class TestController extends InlineChatController { static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT]; - static INIT_SEQUENCE_AUTO_SEND: readonly State[] = [...this.INIT_SEQUENCE, State.MAKE_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]; + static INIT_SEQUENCE_AUTO_SEND: readonly State[] = [...this.INIT_SEQUENCE, State.SHOW_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]; private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; @@ -102,20 +118,27 @@ suite('InteractiveChatController', function () { setup(function () { - contextKeyService = new MockContextKeyService(); - inlineChatService = new InlineChatServiceImpl(contextKeyService); - - configurationService = new TestConfigurationService(); - configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); - configurationService.setUserConfiguration('inlineChat', { mode: 'livePreview' }); - configurationService.setUserConfiguration('editor', {}); - const serviceCollection = new ServiceCollection( + [IConfigurationService, new TestConfigurationService()], + [IChatVariablesService, new MockChatVariablesService()], + [ILogService, new NullLogService()], + [ITelemetryService, NullTelemetryService], + [IExtensionService, new TestExtensionService()], + [IContextKeyService, new MockContextKeyService()], + [IViewsService, new TestExtensionService()], + [IChatContributionService, new TestExtensionService()], + [IWorkspaceContextService, new TestContextService()], + [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], + [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], + [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], + [IChatService, new SyncDescriptor(ChatService)], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], - [IInlineChatService, inlineChatService], + [IChatAgentService, new SyncDescriptor(ChatAgentService)], + [IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], + [ICommandService, new SyncDescriptor(TestCommandService)], [IInlineChatSavingService, new class extends mock() { override markChanged(session: Session): void { // noop @@ -145,15 +168,40 @@ suite('InteractiveChatController', function () { }] ); - instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); + + configurationService = instaService.get(IConfigurationService) as TestConfigurationService; + configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); + configurationService.setUserConfiguration('inlineChat', { mode: 'livePreview' }); + configurationService.setUserConfiguration('editor', {}); + + contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; + + inlineChatService = instaService.get(IInlineChatService) as InlineChatServiceImpl; + + const chatAgentService = instaService.get(IChatAgentService); + + store.add(chatAgentService.registerDynamicAgent({ + extensionId: nullExtensionDescription.identifier, + id: 'testAgent', + name: 'testAgent', + isDefault: true, + locations: [ChatAgentLocation.Panel], + metadata: {}, + slashCommands: [] + }, { + async invoke(request, progress, history, token) { + return {}; + }, + })); inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); editor = store.add(instantiateTestCodeEditor(instaService, model)); store.add(inlineChatService.addProvider({ - debugName: 'Unit Test', - label: 'Unit Test', + extensionId: nullExtensionDescription.identifier, + label: 'Unit Test Default', prepareInlineChatSession() { return { id: Math.random() @@ -177,7 +225,8 @@ suite('InteractiveChatController', function () { ctrl?.dispose(); }); - ensureNoDisposablesAreLeakedInTestSuite(); + // TODO@jrieken re-enable, looks like List/ChatWidget is leaking + // ensureNoDisposablesAreLeakedInTestSuite(); test('creation, not showing anything', function () { ctrl = instaService.createInstance(TestController, editor); @@ -203,8 +252,8 @@ suite('InteractiveChatController', function () { editor.setSelection(new Range(1, 1, 1, 3)); ctrl = instaService.createInstance(TestController, editor); - const d = inlineChatService.addProvider({ - debugName: 'Unit Test', + store.add(inlineChatService.addProvider({ + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -214,7 +263,7 @@ suite('InteractiveChatController', function () { provideResponse(session, request) { throw new Error(); } - }); + })); ctrl.run({}); await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); @@ -224,7 +273,6 @@ suite('InteractiveChatController', function () { assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3)); await ctrl.cancelSession(); - d.dispose(); }); test('wholeRange expands to whole lines, session provided', async function () { @@ -232,8 +280,8 @@ suite('InteractiveChatController', function () { editor.setSelection(new Range(1, 1, 1, 1)); ctrl = instaService.createInstance(TestController, editor); - const d = inlineChatService.addProvider({ - debugName: 'Unit Test', + store.add(inlineChatService.addProvider({ + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -244,7 +292,7 @@ suite('InteractiveChatController', function () { provideResponse(session, request) { throw new Error(); } - }); + })); ctrl.run({}); await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); @@ -254,7 +302,6 @@ suite('InteractiveChatController', function () { assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3)); await ctrl.cancelSession(); - d.dispose(); }); test('typing outside of wholeRange finishes session', async function () { @@ -282,8 +329,8 @@ suite('InteractiveChatController', function () { editor.setSelection(new Range(3, 1, 3, 1)); - const d = inlineChatService.addProvider({ - debugName: 'Unit Test', + store.add(inlineChatService.addProvider({ + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -301,8 +348,8 @@ suite('InteractiveChatController', function () { }] }; } - }); - store.add(d); + })); + ctrl = instaService.createInstance(TestController, editor); const p = ctrl.waitFor(TestController.INIT_SEQUENCE); const r = ctrl.run({ message: 'GENGEN', autoSend: false }); @@ -314,8 +361,7 @@ suite('InteractiveChatController', function () { assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial ctrl.acceptInput(); - - await ctrl.waitFor([State.MAKE_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + await ctrl.waitFor([State.SHOW_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3)); @@ -324,8 +370,8 @@ suite('InteractiveChatController', function () { }); test('Stuck inline chat widget #211', async function () { - const d = inlineChatService.addProvider({ - debugName: 'Unit Test', + store.add(inlineChatService.addProvider({ + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -336,10 +382,9 @@ suite('InteractiveChatController', function () { provideResponse(session, request) { return new Promise(() => { }); } - }); - store.add(d); + })); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.MAKE_REQUEST]); + const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); const r = ctrl.run({ message: 'Hello', autoSend: true }); await p; @@ -351,8 +396,8 @@ suite('InteractiveChatController', function () { test('[Bug] Inline Chat\'s streaming pushed broken iterations to the undo stack #2403', async function () { - const d = inlineChatService.addProvider({ - debugName: 'Unit Test', + store.add(inlineChatService.addProvider({ + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -371,13 +416,12 @@ suite('InteractiveChatController', function () { edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] }; } - }); + })); const valueThen = editor.getModel().getValue(); - store.add(d); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.MAKE_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'Hello', autoSend: true }); await p; ctrl.acceptSession(); @@ -391,14 +435,14 @@ suite('InteractiveChatController', function () { - test('UI is streaming edits minutes after the response is finished #3345', async function () { + test.skip('UI is streaming edits minutes after the response is finished #3345', async function () { configurationService.setUserConfiguration(InlineChatConfigKeys.Mode, EditMode.Live); return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => { - const d = inlineChatService.addProvider({ - debugName: 'Unit Test', + store.add(inlineChatService.addProvider({ + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -417,15 +461,14 @@ suite('InteractiveChatController', function () { throw new Error('Too long'); } - }); + })); // let modelChangeCounter = 0; // store.add(editor.getModel().onDidChangeContent(() => { modelChangeCounter++; })); - store.add(d); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.MAKE_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'Hello', autoSend: true }); await p; @@ -448,7 +491,7 @@ suite('InteractiveChatController', function () { // NO manual edits -> cancel ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.MAKE_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'GENERATED', autoSend: true }); await p; @@ -464,7 +507,7 @@ suite('InteractiveChatController', function () { // manual edits -> finish ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.MAKE_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.APPLY_RESPONSE, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'GENERATED', autoSend: true }); await p; @@ -485,7 +528,7 @@ suite('InteractiveChatController', function () { const requests: IInlineChatRequest[] = []; store.add(inlineChatService.addProvider({ - debugName: 'Unit Test', + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -512,68 +555,14 @@ suite('InteractiveChatController', function () { configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Live }); await makeRequest(); - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.LivePreview }); - await makeRequest(); configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Preview }); await makeRequest(); - assert.strictEqual(requests.length, 3); + assert.strictEqual(requests.length, 2); assert.strictEqual(requests[0].previewDocument.toString(), model.uri.toString()); // live - assert.strictEqual(requests[1].previewDocument.toString(), model.uri.toString()); // live preview - assert.strictEqual(requests[2].previewDocument.scheme, Schemas.vscode); // preview - assert.strictEqual(requests[2].previewDocument.authority, 'inline-chat'); - }); - - test('start with existing exchange', async function () { - - // don't call this provider - let providerCalled = 0; - store.add(inlineChatService.addProvider({ - debugName: 'Unit Test', - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(_session, request) { - providerCalled++; - return undefined; - } - })); - - // use precooked response - const response = { - id: 1, - type: InlineChatResponseType.EditorEdit, - message: new MarkdownString('MD-message'), - edits: [{ - text: 'Precooked Response\n', - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } - }] - - } satisfies IInlineChatEditResponse; - - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Live }); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); - ctrl.run({ existingExchange: { prompt: 'Hello', response } }); - - await p; - - assert.strictEqual(providerCalled, 0); - assert.ok(ctrl.getWidgetPosition() !== undefined); - - assert.ok(ctrl.getMessage() === 'MD-message'); - assert.equal(model.getLineContent(1), 'Precooked Response'); - - await ctrl.cancelSession(); - await ctrl.joinCurrentRun(); - - assert.ok(ctrl.getWidgetPosition() === undefined); - + assert.strictEqual(requests[1].previewDocument.scheme, Schemas.vscode); // preview + assert.strictEqual(requests[1].previewDocument.authority, 'inline-chat'); }); }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 53b0cb87519..0302001fda3 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { TestDiffProviderFactoryService } from 'vs/editor/browser/diff/testDiffProviderFactoryService'; +import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; import { Range } from 'vs/editor/common/core/range'; @@ -27,7 +27,7 @@ import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/co import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; import { HunkState, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; @@ -43,6 +43,25 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { TestWorkerService } from './testWorkerService'; +import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +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 { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { IChatSlashCommandService, ChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatWidgetHistoryService, ChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; +import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { TestExtensionService, TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IChatAgentService, ChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; suite('ReplyResponse', function () { @@ -94,14 +113,30 @@ suite('InlineChatSession', function () { setup(function () { const contextKeyService = new MockContextKeyService(); - inlineChatService = new InlineChatServiceImpl(contextKeyService); + const serviceCollection = new ServiceCollection( + [IConfigurationService, new TestConfigurationService()], + [IChatVariablesService, new MockChatVariablesService()], + [ILogService, new NullLogService()], + [ITelemetryService, NullTelemetryService], + [IExtensionService, new TestExtensionService()], + [IContextKeyService, new MockContextKeyService()], + [IViewsService, new TestExtensionService()], + [IChatContributionService, new TestExtensionService()], + [IWorkspaceContextService, new TestContextService()], + [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], + [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], + [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], + [IChatService, new SyncDescriptor(ChatService)], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], - [IInlineChatService, inlineChatService], + [IChatContributionService, new MockChatContributionService()], + [IChatAgentService, new SyncDescriptor(ChatAgentService)], + [IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)], [IContextKeyService, contextKeyService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], + [ICommandService, new SyncDescriptor(TestCommandService)], [IInlineChatSavingService, new class extends mock() { override markChanged(session: Session): void { // noop @@ -131,8 +166,29 @@ suite('InlineChatSession', function () { }] ); + + + instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + + inlineChatService = instaService.get(IInlineChatService) as InlineChatServiceImpl; + inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); + + instaService.get(IChatAgentService).registerDynamicAgent({ + extensionId: nullExtensionDescription.identifier, + id: 'testAgent', + name: 'testAgent', + isDefault: true, + locations: [ChatAgentLocation.Panel], + metadata: {}, + slashCommands: [] + }, { + async invoke() { + return {}; + } + }); + store.add(inlineChatService.addProvider({ - debugName: 'Unit Test', + extensionId: nullExtensionDescription.identifier, label: 'Unit Test', prepareInlineChatSession() { return { @@ -150,10 +206,6 @@ suite('InlineChatSession', function () { }; } })); - - instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); - inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); - model = store.add(instaService.get(IModelService).createModel('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven', null)); editor = store.add(instantiateTestCodeEditor(instaService, model)); }); diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 05d7d253673..3a6737053eb 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -12,7 +12,7 @@ import { extname, isEqual } from 'vs/base/common/resources'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ITextModel } from 'vs/editor/common/model'; @@ -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'; @@ -254,8 +254,12 @@ 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(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/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index f9c766ca849..95e4406ce2e 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -8,9 +8,9 @@ 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/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -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/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts index e8317893d1e..8dd727d49c6 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditorInput.ts +++ b/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/src/vs/workbench/contrib/issue/browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.contribution.ts index 7d19d1cd3c6..28751d1c2c8 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts +++ b/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/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts new file mode 100644 index 00000000000..37d52199b5b --- /dev/null +++ b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * 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"); + const productFilter = matchesFuzzy(filter, productLabel, true); + const marketPlaceFilter = matchesFuzzy(filter, marketPlaceLabel, true); + + // Add product pick if product filter matches + if (productFilter) { + issuePicksConst.push({ + label: productLabel, + ariaLabel: productLabel, + highlights: { label: productFilter }, + accept: () => this.commandService.executeCommand('workbench.action.openIssueReporter', { issueSource: IssueSource.VSCode }) + }); + } + + // Add marketplace pick if marketplace filter matches + if (marketPlaceFilter) { + issuePicksConst.push({ + label: marketPlaceLabel, + ariaLabel: marketPlaceLabel, + highlights: { label: marketPlaceFilter }, + accept: () => this.commandService.executeCommand('workbench.action.openIssueReporter', { issueSource: IssueSource.Marketplace }) + }); + } + + issuePicksConst.push({ 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; + } + + const highlights = matchesFuzzy(filter, label, true); + if (highlights) { + return { + label, + highlights: { label: highlights }, + buttons, + trigger, + accept + }; + } + return undefined; + } +} diff --git a/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/src/vs/workbench/contrib/issue/common/issue.contribution.ts index 05518d0f4aa..9e6ee979ac9 100644 --- a/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/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/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 05bc0632624..d6c76d7fbd9 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/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/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index a444b099637..56e24b867e2 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -324,7 +324,7 @@ class LanguageStatus { href: URI.from({ scheme: 'command', path: command.id, query: command.arguments && JSON.stringify(command.arguments) }).toString() - }, undefined, this._openerService)); + }, { hoverDelegate: nativeHoverDelegate }, this._openerService)); } // -- pin diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index f7fb23f59c6..4354ad022df 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/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/src/vs/workbench/contrib/logs/browser/logs.contribution.ts b/src/vs/workbench/contrib/logs/browser/logs.contribution.ts index 23f019bac51..15bd494c20a 100644 --- a/src/vs/workbench/contrib/logs/browser/logs.contribution.ts +++ b/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/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts b/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts index 9feccec6965..522b64a8cd6 100644 --- a/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts +++ b/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/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 19257411641..b96ba30f68c 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/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/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 519fab7f93a..3ececbb1223 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/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/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 8ac086cada8..9b18b5cd028 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/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/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index c4cf0aa94fe..095a2978564 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/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/src/vs/workbench/contrib/markers/browser/markersTable.ts b/src/vs/workbench/contrib/markers/browser/markersTable.ts index dc454d054ed..44467e7e01d 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTable.ts +++ b/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 { @@ -74,7 +75,7 @@ class MarkerSeverityColumnRenderer implements ITableRenderer action.id === QuickFixAction.ID ? this.instantiationService.createInstance(QuickFixActionViewItem, action) : undefined + actionViewItemProvider: (action: IAction, options) => action.id === QuickFixAction.ID ? this.instantiationService.createInstance(QuickFixActionViewItem, action, options) : undefined }); return { actionBar, icon }; @@ -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/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 4b7a07b3b17..a3556f80678 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/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( @@ -295,7 +298,7 @@ class MarkerWidget extends Disposable { ) { super(); this.actionBar = this._register(new ActionBar(dom.append(parent, dom.$('.actions')), { - actionViewItemProvider: (action: IAction) => action.id === QuickFixAction.ID ? _instantiationService.createInstance(QuickFixActionViewItem, action) : undefined + actionViewItemProvider: (action: IAction, options) => action.id === QuickFixAction.ID ? _instantiationService.createInstance(QuickFixActionViewItem, action, options) : undefined })); // wrap the icon in a container that get the icon color as foreground color. That way, if the @@ -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 { @@ -342,9 +346,9 @@ class MarkerWidget extends Disposable { private renderMultilineActionbar(marker: Marker, parent: HTMLElement): void { const multilineActionbar = this.disposables.add(new ActionBar(dom.append(parent, dom.$('.multiline-actions')), { - actionViewItemProvider: (action) => { + actionViewItemProvider: (action, options) => { if (action.id === toggleMultilineAction) { - return new ToggleMultilineActionViewItem(undefined, action, { icon: true }); + return new ToggleMultilineActionViewItem(undefined, action, { ...options, icon: true }); } return undefined; } @@ -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/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index d2853645051..b43d06358f8 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -7,7 +7,7 @@ import { ArrayQueue, CompareResult } from 'vs/base/common/arrays'; import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, autorunOpts, observableFromEvent } from 'vs/base/common/observable'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts index 9d349b594ec..b751b6bc0ad 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts @@ -6,7 +6,7 @@ import { h, reset } from 'vs/base/browser/dom'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, IReader, observableFromEvent, observableSignal, observableSignalFromEvent, transaction } from 'vs/base/common/observable'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; export class EditorGutter extends Disposable { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 38266ed0600..8cd243e3777 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -9,7 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IObservable, autorun, derived, observableFromEvent } from 'vs/base/common/observable'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts index f6ccc0cf7a3..c6d9664b4be 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/lineAlignment.ts +++ b/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/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 45af28b94f0..3609b0046fa 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -108,6 +108,7 @@ export class MergeEditor extends AbstractTextEditor { private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); constructor( + group: IEditorGroup, @IInstantiationService instantiation: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @@ -121,7 +122,7 @@ export class MergeEditor extends AbstractTextEditor { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(MergeEditor.ID, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override dispose(): void { @@ -354,7 +355,7 @@ export class MergeEditor extends AbstractTextEditor { // all empty -> replace this editor with a normal editor for result that.editorService.replaceEditors( [{ editor: input, replacement: { resource: input.result, options: { preserveFocus: true } }, forceReplaceDirty: true }], - that.group ?? that.editorGroupService.activeGroup + that.group ); } }); @@ -467,8 +468,8 @@ export class MergeEditor extends AbstractTextEditor { return super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { if (visible) { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts index bbbe978f302..94857ee4726 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { autorunWithStore, IObservable } from 'vs/base/common/observable'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { DocumentLineRangeMap } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; import { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils'; diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts index ae714c10766..85c475e3c2f 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts +++ b/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/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index c1491431828..ffe7a8738f5 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -5,8 +5,8 @@ import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { MultiDiffEditorWidget } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidget'; -import { IResourceLabel, IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditorWidget/workbenchUIElementFactory'; +import { MultiDiffEditorWidget } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget'; +import { IResourceLabel, IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; @@ -19,11 +19,11 @@ 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/multiDiffEditorWidget/multiDiffEditorViewModel'; -import { IMultiDiffEditorOptions, IMultiDiffEditorViewState } from 'vs/editor/browser/widget/multiDiffEditorWidget/multiDiffEditorWidgetImpl'; +import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; +import { IMultiDiffEditorOptions, IMultiDiffEditorViewState } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditor } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; @@ -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/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts index 77a91525426..de90619669f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts @@ -14,14 +14,18 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { changeCellToKind, computeCellLinesContents, copyCellRange, joinCellsWithSurrounds, joinSelectedCells, moveCellRange } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; -import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellOutputViewModel, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellEditType, CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { Range } from 'vs/editor/common/core/range'; +import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; +import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; //#region Move/Copy cells const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp'; @@ -353,6 +357,7 @@ const COLLAPSE_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.collapseAllCellOutpu const EXPAND_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.expandAllCellOutputs'; const TOGGLE_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.toggleOutputs'; const TOGGLE_CELL_OUTPUT_SCROLLING = 'notebook.cell.toggleOutputScrolling'; +export const OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID = 'notebook.cell.openFailureActions'; registerAction2(class CollapseCellInputAction extends NotebookMultiCellAction { constructor() { @@ -579,6 +584,45 @@ registerAction2(class ToggleCellOutputScrolling extends NotebookMultiCellAction } }); +registerAction2(class ExpandAllCellOutputsAction extends NotebookCellAction { + constructor() { + super({ + id: OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID, + title: localize2('notebookActions.cellFailureActions', "Show Cell Failure Actions"), + precondition: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()), + f1: true, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()), + primary: KeyMod.CtrlCmd | KeyCode.Period, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + if (context.cell instanceof CodeCellViewModel) { + const error = context.cell.cellDiagnostics.ErrorDetails; + if (error?.location) { + const location = Range.lift({ + startLineNumber: error.location.startLineNumber + 1, + startColumn: error.location.startColumn + 1, + endLineNumber: error.location.endLineNumber + 1, + endColumn: error.location.endColumn + 1 + }); + context.notebookEditor.setCellEditorSelection(context.cell, Range.lift(location)); + const editor = findTargetCellEditor(context, context.cell); + if (editor) { + const controller = CodeActionController.get(editor); + controller?.manualTriggerAtCurrentPosition( + localize('cellCommands.quickFix.noneMessage', "No code actions available"), + CodeActionTriggerSource.Default, + { include: CodeActionKind.QuickFix }); + } + } + } + } +}); + //#endregion function forEachCell(editor: INotebookEditor, callback: (cell: ICellViewModel, index: number) => void) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts new file mode 100644 index 00000000000..d04037b939f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; +import { IRange } from 'vs/editor/common/core/range'; +import { ICellExecutionError, ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { Iterable } from 'vs/base/common/iterator'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { Emitter, Event } from 'vs/base/common/event'; + + +export class CellDiagnostics extends Disposable { + + private readonly _onDidDiagnosticsChange = new Emitter(); + readonly onDidDiagnosticsChange: Event = this._onDidDiagnosticsChange.event; + + static ID: string = 'workbench.notebook.cellDiagnostics'; + + private enabled = false; + private listening = false; + private errorDetails: ICellExecutionError | undefined = undefined; + public get ErrorDetails() { + return this.errorDetails; + } + + constructor( + private readonly cell: CodeCellViewModel, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, + @IMarkerService private readonly markerService: IMarkerService, + @IInlineChatService private readonly inlineChatService: IInlineChatService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + if (cell.viewType !== 'interactive') { + this.updateEnabled(); + + this._register(inlineChatService.onDidChangeProviders(() => this.updateEnabled())); + this._register(configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(NotebookSetting.cellFailureDiagnostics)) { + this.updateEnabled(); + } + })); + } + } + + private updateEnabled() { + const settingEnabled = this.configurationService.getValue(NotebookSetting.cellFailureDiagnostics); + if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.inlineChatService.getAllProvider()))) { + this.enabled = false; + this.clear(); + } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.inlineChatService.getAllProvider())) { + this.enabled = true; + if (!this.listening) { + this.listening = true; + this._register(this.notebookExecutionStateService.onDidChangeExecution((e) => this.handleChangeExecutionState(e))); + } + } + } + + private handleChangeExecutionState(e: ICellExecutionStateChangedEvent | IExecutionStateChangedEvent) { + if (this.enabled && e.type === NotebookExecutionType.cell && e.affectsCell(this.cell.uri)) { + if (!!e.changed) { + // cell is running + this.clear(); + } else { + this.setDiagnostics(); + } + } + } + + public clear() { + if (this.ErrorDetails) { + this.markerService.changeOne(CellDiagnostics.ID, this.cell.uri, []); + this.errorDetails = undefined; + this._onDidDiagnosticsChange.fire(); + } + } + + private setDiagnostics() { + const metadata = this.cell.model.internalMetadata; + if (!metadata.lastRunSuccess && metadata?.error?.location) { + const marker = this.createMarkerData(metadata.error.message, metadata.error.location); + this.markerService.changeOne(CellDiagnostics.ID, this.cell.uri, [marker]); + this.errorDetails = metadata.error; + this._onDidDiagnosticsChange.fire(); + } + } + + private createMarkerData(message: string, location: IRange): IMarkerData { + return { + severity: 8, + message: message, + startLineNumber: location.startLineNumber + 1, + startColumn: location.startColumn + 1, + endLineNumber: location.endLineNumber + 1, + endColumn: location.endColumn + 1, + source: 'Cell Execution Error' + }; + } + + override dispose() { + super.dispose(); + this.clear(); + } + +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts index 34a10466dd1..26fb72cc285 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -19,6 +19,9 @@ import { CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecuti import { INotebookCellExecution, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export function formatCellDuration(duration: number, showMilliseconds: boolean = true): string { if (showMilliseconds && duration < 1000) { @@ -333,3 +336,60 @@ class TimerCellStatusBarItem extends Disposable { this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); } } + +export class DiagnosticCellStatusBarContrib extends Disposable implements INotebookEditorContribution { + static id: string = 'workbench.notebook.statusBar.diagtnostic'; + + constructor( + notebookEditor: INotebookEditor, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(); + this._register(new NotebookStatusBarController(notebookEditor, (vm, cell) => + cell instanceof CodeCellViewModel ? + instantiationService.createInstance(DiagnosticCellStatusBarItem, vm, cell) : + Disposable.None + )); + } +} +registerNotebookContribution(DiagnosticCellStatusBarContrib.id, DiagnosticCellStatusBarContrib); + + +class DiagnosticCellStatusBarItem extends Disposable { + private _currentItemIds: string[] = []; + + constructor( + private readonly _notebookViewModel: INotebookViewModel, + private readonly cell: CodeCellViewModel, + @IKeybindingService private readonly keybindingService: IKeybindingService + ) { + super(); + this._update(); + this._register(this.cell.cellDiagnostics.onDidDiagnosticsChange(() => this._update())); + } + + private async _update() { + let item: INotebookCellStatusBarItem | undefined; + + if (!!this.cell.cellDiagnostics.ErrorDetails) { + const keybinding = this.keybindingService.lookupKeybinding(OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID)?.getLabel(); + const tooltip = localize('notebook.cell.status.diagnostic', "Quick Actions {0}", `(${keybinding})`); + + item = { + text: `$(sparkle)`, + tooltip, + alignment: CellStatusbarAlignment.Left, + command: OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID, + priority: Number.MAX_SAFE_INTEGER - 1 + }; + } + + const items = item ? [item] : []; + this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this.cell.handle, items }]); + } + + override dispose() { + super.dispose(); + this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this.cell.handle, items: [] }]); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 73f0201827b..2c4a1d43436 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/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,41 @@ 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, 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; + } + if (cell.inputInOutputIsFocused) { + editor.selectInputContents(cell); + } else { + editor.selectOutputContent(cell); + } + return true; + }); + + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 480033afb33..0c4b949b65e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -40,6 +40,7 @@ import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { Disposable } from 'vs/base/common/lifecycle'; import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -63,11 +64,12 @@ const NOTEBOOK_FIND_IN_CODE_OUTPUT = nls.localize('notebook.find.filter.findInCo const NOTEBOOK_FIND_WIDGET_INITIAL_WIDTH = 318; const NOTEBOOK_FIND_WIDGET_INITIAL_HORIZONTAL_PADDING = 4; class NotebookFindFilterActionViewItem extends DropdownMenuActionViewItem { - constructor(readonly filters: NotebookFindFilters, action: IAction, actionRunner: IActionRunner, @IContextMenuService contextMenuService: IContextMenuService) { + constructor(readonly filters: NotebookFindFilters, action: IAction, options: IActionViewItemOptions, actionRunner: IActionRunner, @IContextMenuService contextMenuService: IContextMenuService) { super(action, { getActions: () => this.getActions() }, contextMenuService, { + ...options, actionRunner, classNames: action.class, anchorAlignmentProvider: () => AnchorAlignment.RIGHT @@ -184,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; @@ -196,9 +206,9 @@ export class NotebookFindInputFilterButton extends Disposable { private createFilters(container: HTMLElement): void { this._actionbar = this._register(new ActionBar(container, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action.id === this._filtersAction.id) { - return this.instantiationService.createInstance(NotebookFindFilterActionViewItem, this.filters, action, new ActionRunner()); + return this.instantiationService.createInstance(NotebookFindFilterActionViewItem, this.filters, action, options, new ActionRunner()); } return undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index 52eea945ec6..cb9e490f91d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts index b9cdadad7ff..13bc42a678d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesDataSource.ts @@ -21,6 +21,7 @@ export interface INotebookVariableElement { readonly name: string; readonly value: string; readonly type?: string; + readonly interfaces?: string[]; readonly expression?: string; readonly language?: string; readonly indexedChildrenCount: number; @@ -82,13 +83,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 +112,7 @@ export class NotebookVariableDataSource implements IAsyncDataSource, _index: number, template: NotebookOutlineTemplate, _height: number | undefined): void { @@ -79,14 +105,17 @@ class NotebookOutlineRenderer implements ITreeRenderer= 8) { // symbol + template.iconClass.className = 'element-icon ' + ThemeIcon.asClassNameArray(node.element.icon).join(' '); + } else if (isCodeCell && this._themeService.getFileIconTheme().hasFileIcons && !node.element.isExecuting) { template.iconClass.className = ''; extraClasses.push(...getIconClassesForLanguageId(node.element.cell.language ?? '')); } else { template.iconClass.className = 'element-icon ' + ThemeIcon.asClassNameArray(node.element.icon).join(' '); } - template.iconLabel.setLabel(node.element.label, undefined, options); + template.iconLabel.setLabel(' ' + node.element.label, undefined, options); const { markerInfo } = node.element; @@ -118,11 +147,113 @@ 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); + template.actionMenu.style.padding = '0 0.8em 0 0.4em'; + } } 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 { @@ -183,7 +314,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; @@ -249,9 +380,13 @@ export class NotebookCellOutline implements IOutline { })); installSelectionListener(); - const treeDataSource: IDataSource = { getChildren: parent => parent instanceof NotebookCellOutline ? (this._outlineProvider?.entries ?? []) : parent.children }; + const treeDataSource: IDataSource = { + getChildren: parent => { + return this.getChildren(parent, _configurationService); + } + }; 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 = { @@ -284,6 +419,29 @@ export class NotebookCellOutline implements IOutline { }; } + *getChildren(parent: OutlineEntry | NotebookCellOutline, configurationService: IConfigurationService): Iterable { + const showCodeCells = configurationService.getValue(NotebookSetting.outlineShowCodeCells); + const showCodeCellSymbols = configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + const showMarkdownHeadersOnly = configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); + + for (const entry of parent instanceof NotebookCellOutline ? (this._outlineProvider?.entries ?? []) : parent.children) { + if (entry.cell.cellKind === CellKind.Markup) { + if (!showMarkdownHeadersOnly) { + yield entry; + } else if (entry.level < 7) { + yield entry; + } + + } else if (showCodeCells && entry.cell.cellKind === CellKind.Code) { + if (showCodeCellSymbols) { + yield entry; + } else if (entry.level === 7) { + yield entry; + } + } + } + } + async setFullSymbols(cancelToken: CancellationToken) { await this._outlineProvider?.setFullSymbols(cancelToken); } @@ -396,36 +554,138 @@ export class NotebookOutlineCreator implements IOutlineCreator | undefined> { const outline = this._instantiationService.createInstance(NotebookCellOutline, editor, target); - const showAllSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); - if (target === OutlineTarget.QuickPick && showAllSymbols) { + const showAllGotoSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + const showAllOutlineSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + if (target === OutlineTarget.QuickPick && showAllGotoSymbols) { + await outline.setFullSymbols(cancelToken); + } else if (target === OutlineTarget.OutlinePane && showAllOutlineSymbols) { await outline.setFullSymbols(cancelToken); } + return outline; } } -Registry.as(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', order: 100, type: 'object', 'properties': { - 'notebook.outline.showCodeCells': { + [NotebookSetting.outlineShowMarkdownHeadersOnly]: { + type: 'boolean', + default: true, + markdownDescription: localize('outline.showMarkdownHeadersOnly', "When enabled, notebook outline will show only markdown cells containing a header.") + }, + [NotebookSetting.outlineShowCodeCells]: { type: 'boolean', default: false, - markdownDescription: localize('outline.showCodeCells', "When enabled notebook outline shows code cells.") + markdownDescription: localize('outline.showCodeCells', "When enabled, notebook outline shows code cells.") }, - 'notebook.breadcrumbs.showCodeCells': { + [NotebookSetting.outlineShowCodeCellSymbols]: { type: 'boolean', default: true, - markdownDescription: localize('breadcrumbs.showCodeCells', "When enabled notebook breadcrumbs contain code cells.") + markdownDescription: localize('outline.showCodeCellSymbols', "When enabled, notebook outline shows code cell symbols. Relies on `notebook.outline.showCodeCells` being enabled.") + }, + [NotebookSetting.breadcrumbsShowCodeCells]: { + type: 'boolean', + default: true, + markdownDescription: localize('breadcrumbs.showCodeCells', "When enabled, notebook breadcrumbs contain code cells.") }, [NotebookSetting.gotoSymbolsAllSymbols]: { type: 'boolean', default: true, - markdownDescription: localize('notebook.gotoSymbols.showAllSymbols', "When enabled the Go to Symbol Quick Pick will display full code symbols from the notebook, as well as Markdown headers.") + markdownDescription: localize('notebook.gotoSymbols.showAllSymbols', "When enabled, the Go to Symbol Quick Pick will display full code symbols from the notebook, as well as Markdown headers.") }, } }); + +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 ToggleShowMarkdownHeadersOnly extends Action2 { + constructor() { + super({ + id: 'notebook.outline.toggleShowMarkdownHeadersOnly', + title: localize('toggleShowMarkdownHeadersOnly', "Markdown Headers Only"), + f1: false, + toggled: { + condition: ContextKeyExpr.equals('config.notebook.outline.showMarkdownHeadersOnly', true) + }, + menu: { + id: MenuId.NotebookOutlineFilter, + group: '0_markdown_cells', + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const configurationService = accessor.get(IConfigurationService); + const showMarkdownHeadersOnly = configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); + configurationService.updateValue(NotebookSetting.outlineShowMarkdownHeadersOnly, !showMarkdownHeadersOnly); + } +}); + + +registerAction2(class ToggleCodeCellEntries extends Action2 { + constructor() { + super({ + id: 'notebook.outline.toggleCodeCells', + title: localize('toggleCodeCells', "Code Cells"), + f1: false, + toggled: { + condition: ContextKeyExpr.equals('config.notebook.outline.showCodeCells', true) + }, + menu: { + id: MenuId.NotebookOutlineFilter, + order: 1, + group: '1_code_cells', + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const configurationService = accessor.get(IConfigurationService); + const showCodeCells = configurationService.getValue(NotebookSetting.outlineShowCodeCells); + configurationService.updateValue(NotebookSetting.outlineShowCodeCells, !showCodeCells); + } +}); + +registerAction2(class ToggleCodeCellSymbolEntries extends Action2 { + constructor() { + super({ + id: 'notebook.outline.toggleCodeCellSymbols', + title: localize('toggleCodeCellSymbols', "Code Cell Symbols"), + f1: false, + toggled: { + condition: ContextKeyExpr.equals('config.notebook.outline.showCodeCellSymbols', true) + }, + menu: { + id: MenuId.NotebookOutlineFilter, + order: 2, + group: '1_code_cells', + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const configurationService = accessor.get(IConfigurationService); + const showCodeCellSymbols = configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + configurationService.updateValue(NotebookSetting.outlineShowCodeCellSymbols, !showCodeCellSymbols); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index 0ffd61f705a..31c62508ca9 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 430062261c9..7760ac2916c 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from 'vs/base/common/codicons'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize, localize2 } from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; @@ -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,13 +30,13 @@ 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), - weight: KeybindingWeight.EditorCore + 7, + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, NOTEBOOK_CELL_EDITOR_FOCUSED.negate()), + weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Enter }, menu: { id: MENU_CELL_CHAT_INPUT, - group: 'main', + group: 'navigation', order: 1, when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.negate() } @@ -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, @@ -181,7 +183,7 @@ registerAction2(class extends NotebookAction { icon: Codicon.debugStop, menu: { id: MENU_CELL_CHAT_INPUT, - group: 'main', + group: 'navigation', order: 1, when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST } @@ -202,14 +204,14 @@ registerAction2(class extends NotebookAction { icon: Codicon.close, menu: { id: MENU_CELL_CHAT_WIDGET, - group: 'main', + group: 'navigation', order: 2 } }); } 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', @@ -383,6 +386,18 @@ registerAction2(class extends NotebookAction { ] }, f1: false, + keybinding: { + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_FOCUSED, + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.not(InputFocusedContextKey), + CTX_INLINE_CHAT_HAS_PROVIDER, + ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) + ), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI, + secondary: [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI)], + }, menu: [ { id: MenuId.NotebookCellBetween, @@ -449,12 +464,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 +494,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 +588,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/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 61810983632..d287c272e37 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -6,16 +6,18 @@ 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/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; @@ -29,7 +31,10 @@ 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 { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; 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 +45,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 +67,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 +86,7 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { super(); this._register(inlineChatWidget.onDidChangeHeight(() => { - this.heightInPx = inlineChatWidget.getHeight() + WIDGET_MARGIN_BOTTOM; + this.heightInPx = inlineChatWidget.contentHeight; this._notebookEditor.changeViewZones(accessor => { accessor.layoutZone(id); }); @@ -96,21 +96,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 +148,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 +194,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 +211,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 +232,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 +268,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 +279,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 +315,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) { const window = getWindow(this._widget.domNode); - this._widget.dispose(); - this._widget = undefined; - this._widgetDisposableStore.clear(); + this._disposeWidget(); 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 +405,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito const inlineChatWidget = this._widgetDisposableStore.add(this._instantiationService.createInstance( InlineChatWidget, - fakeParentEditor, + ChatAgentLocation.Notebook, { - 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 +425,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 +443,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 +510,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() { @@ -403,10 +519,13 @@ export class NotebookChatController extends Disposable implements INotebookEdito assertType(this._activeSession); this._warmupRequestCts?.dispose(true); this._warmupRequestCts = undefined; - this._activeSession.addInput(new SessionPrompt(this._widget.inlineChatWidget.value)); + this._activeSession.addInput(new SessionPrompt(this._widget.inlineChatWidget.value, 0, true)); assertType(this._activeSession.lastInput); const value = this._activeSession.lastInput.value; + + this._historyUpdate(value); + const editor = this._widget.parentEditor; const model = editor.getModel(); @@ -495,7 +614,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito if (!progressiveChatResponse) { const message = { message: new MarkdownString(data.markdownFragment, { supportThemeIcons: true, supportHtml: true, isTrusted: false }), - providerId: this._activeSession!.provider.debugName, + providerId: this._activeSession!.provider.label, requestId: request.requestId, }; progressiveChatResponse = this._widget?.inlineChatWidget.updateChatMessage(message, true); @@ -512,7 +631,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 +816,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 +866,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 +891,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 +933,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 +952,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 +983,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 +1085,3 @@ export class EditStrategy { registerNotebookContribution(NotebookChatController.id, NotebookChatController); - diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 087c143716c..3cc7faf8354 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 2ca62a92f6f..ce41420deba 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts index 2dfc3c83bc3..19f30d4185f 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts new file mode 100644 index 00000000000..b4831209b27 --- /dev/null +++ b/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/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts index 745b141de35..c2b1d9cf252 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts @@ -50,4 +50,6 @@ export const fixedDiffEditorOptions: IDiffEditorConstructionOptions = { wordWrap: 'off', diffWordWrap: 'off', diffAlgorithm: 'advanced', + renderSideBySide: true, + useInlineViewWhenSpaceIsLimited: false }; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 23e8dec4478..43b387b9689 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -9,7 +9,7 @@ import { Schemas } from 'vs/base/common/network'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DiffElementViewModelBase, getFormattedMetadataJSON, getFormattedOutputJSON, OutputComparison, outputEqual, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { CellEditType, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -117,9 +117,9 @@ class PropertyHeader extends Disposable { const cellToolbarContainer = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-toolbar')); this._toolbar = new WorkbenchToolBar(cellToolbarContainer, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, undefined, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService, this.accessibilityService); + const item = new CodiconActionViewItem(action, { hoverDelegate: options.hoverDelegate }, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService, this.accessibilityService); return item; } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts index 0f31bdfc6ca..9b15ad922c3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -62,6 +62,15 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe this._onDidChangeState.fire({ outputIsFocusedChanged: true }); } + private _focusInputInOutput: boolean = false; + public get inputInOutputIsFocused(): boolean { + return this._focusInputInOutput; + } + + public set inputInOutputIsFocused(v: boolean) { + this._focusInputInOutput = v; + } + private _outputViewModels: ICellOutputViewModel[]; get outputsViewModels() { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index e2de1a22096..29dc02777d3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts index 6741f2a3ab8..031c2afdc7f 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts index 11aa9bfb6b1..a3cfecd2a6f 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts @@ -17,7 +17,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { DeletedElement, getOptimizedNestedCodeEditorWidgetOptions, InsertElement, ModifiedElement } from 'vs/workbench/contrib/notebook/browser/diff/diffComponents'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -189,9 +189,9 @@ export class CellDiffSideBySideRenderer implements IListRenderer { + actionViewItemProvider: (action, options) => { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, undefined, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService, this.accessibilityService); + const item = new CodiconActionViewItem(action, { hoverDelegate: options.hoverDelegate }, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService, this.accessibilityService); return item; } @@ -248,7 +248,9 @@ export class CellDiffSideBySideRenderer implements IListRenderer .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/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css b/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css index 6e71e660f87..677f9c89ea7 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css +++ b/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/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css b/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css index 28ea1557a52..cb19b73e48b 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css +++ b/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/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 86b3e4f49ca..ce1dc83569c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/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'; @@ -1107,7 +1108,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 }, @@ -1115,6 +1116,11 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('notebook.VariablesView.description', "Enable the experimental notebook variables view within the debug panel."), type: 'boolean', default: false - } + }, + [NotebookSetting.cellFailureDiagnostics]: { + markdownDescription: nls.localize('notebook.cellFailureDiagnostics', "Show available diagnostics for cell failures."), + type: 'boolean', + default: true + }, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts new file mode 100644 index 00000000000..948db4cd3a1 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { observableFromEvent } from 'vs/base/common/observable'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; +import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; +import { CellKind, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; + +export class NotebookAccessibilityProvider extends Disposable implements IListAccessibilityProvider { + private readonly _onDidAriaLabelChange = new Emitter(); + private readonly onDidAriaLabelChange = this._onDidAriaLabelChange.event; + + constructor( + private readonly notebookExecutionStateService: INotebookExecutionStateService, + private readonly viewModel: () => NotebookViewModel | undefined, + private readonly keybindingService: IKeybindingService, + private readonly configurationService: IConfigurationService + ) { + super(); + this._register(Event.debounce( + this.notebookExecutionStateService.onDidChangeExecution, + (last: number[] | undefined, e: ICellExecutionStateChangedEvent | IExecutionStateChangedEvent) => this.mergeEvents(last, e), + 100 + )((cellHandles: number[]) => { + const viewModel = this.viewModel(); + if (viewModel) { + for (const handle of cellHandles) { + const cellModel = viewModel.getCellByHandle(handle); + if (cellModel) { + this._onDidAriaLabelChange.fire(cellModel as CellViewModel); + } + } + } + }, this)); + } + + getAriaLabel(element: CellViewModel) { + const event = Event.filter(this.onDidAriaLabelChange, e => e === element); + return observableFromEvent(event, () => { + const viewModel = this.viewModel(); + if (!viewModel) { + return ''; + } + const index = viewModel.getCellIndex(element); + + if (index >= 0) { + return this.getLabel(index, element); + } + + return ''; + }); + } + + private getLabel(index: number, element: CellViewModel) { + const executionState = this.notebookExecutionStateService.getCellExecution(element.uri)?.state; + const executionLabel = + executionState === NotebookCellExecutionState.Executing + ? ', executing' + : executionState === NotebookCellExecutionState.Pending + ? ', pending' + : ''; + return `Cell ${index}, ${element.cellKind === CellKind.Markup ? 'markdown' : 'code'} cell${executionLabel}`; + } + + getWidgetAriaLabel() { + const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + + if (this.configurationService.getValue(AccessibilityVerbositySettingId.Notebook)) { + return keybinding + ? nls.localize('notebookTreeAriaLabelHelp', "Notebook\nUse {0} for accessibility help", keybinding) + : nls.localize('notebookTreeAriaLabelHelpNoKb', "Notebook\nRun the Open Accessibility Help command for more information", keybinding); + } + return nls.localize('notebookTreeAriaLabel', "Notebook"); + } + + private mergeEvents(last: number[] | undefined, e: ICellExecutionStateChangedEvent | IExecutionStateChangedEvent): number[] { + const viewModel = this.viewModel(); + const result = last || []; + if (viewModel && e.type === NotebookExecutionType.cell && e.affectsNotebook(viewModel.uri)) { + if (result.indexOf(e.cellHandle) < 0) { + result.push(e.cellHandle); + } + } + return result; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 4f6205e00cc..35eb6e5800e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -128,6 +128,7 @@ export interface IGenericCellViewModel { metadata: NotebookCellMetadata; outputIsHovered: boolean; outputIsFocused: boolean; + inputInOutputIsFocused: boolean; outputsViewModels: ICellOutputViewModel[]; getOutputOffset(index: number): number; updateOutputHeight(index: number, height: number, source?: string): void; @@ -449,6 +450,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 +583,16 @@ 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; + /** + * Select the active input element of the first focused output of the cell. + * Implementation of Ctrl+A for an input element in an output item. + */ + selectInputContents(cell: ICellViewModel): void; readonly onDidReceiveMessage: Event; @@ -629,6 +641,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/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 10ae44a781d..b4802da503f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/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'; @@ -47,10 +47,11 @@ import { streamToBuffer } from 'vs/base/common/buffer'; import { ILogService } from 'vs/platform/log/common/log'; import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; 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; @@ -74,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, @@ -93,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))); @@ -137,10 +142,10 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._rootElement.id = `notebook-editor-element-${generateUuid()}`; } - override getActionViewItem(action: IAction): IActionViewItem | undefined { + override getActionViewItem(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { if (action.id === SELECT_KERNEL_ID) { // this is being disposed by the consumer - return this._instantiationService.createInstance(NotebooKernelActionViewItem, action, this); + return this._instantiationService.createInstance(NotebooKernelActionViewItem, action, this, options); } return undefined; } @@ -149,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); @@ -202,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)); @@ -212,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 || ''); @@ -318,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(); @@ -337,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)); @@ -509,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; } @@ -522,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; } @@ -544,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/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 47a455a84a1..a2bc9146b8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -41,7 +41,7 @@ import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestCont import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -100,9 +100,10 @@ import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/brows import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; -import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { PreventDefaultContextMenuItemsContextKeyName } from 'vs/workbench/contrib/webview/browser/webview.contribution'; +import { NotebookAccessibilityProvider } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider'; const $ = DOM.$; @@ -300,7 +301,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD @IContextMenuService private readonly contextMenuService: IContextMenuService, @ITelemetryService private readonly telemetryService: ITelemetryService, @INotebookExecutionService private readonly notebookExecutionService: INotebookExecutionService, - @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, @IEditorProgressService private editorProgressService: IEditorProgressService, @INotebookLoggingService readonly logService: INotebookLoggingService, @IKeybindingService readonly keybindingService: IKeybindingService, @@ -392,7 +393,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 +411,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(); @@ -423,6 +421,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._outputInputFocus = NOTEBOOK_OUPTUT_INPUT_FOCUSED.bindTo(this.scopedContextKeyService); this._editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.scopedContextKeyService); this._cursorNavMode = NOTEBOOK_CURSOR_NAVIGATION_MODE.bindTo(this.scopedContextKeyService); + // Never display the native cut/copy context menu items in notebooks + new RawContextKey(PreventDefaultContextMenuItemsContextKeyName, false).bindTo(this.scopedContextKeyService).set(true); this._editorEditable.set(!creationOptions.isReadOnly); @@ -899,16 +899,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._listDelegate = this.instantiationService.createInstance(NotebookCellListDelegate, DOM.getWindow(this.getDomNode())); this._register(this._listDelegate); - const createNotebookAriaLabel = () => { - const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); - - if (this.configurationService.getValue(AccessibilityVerbositySettingId.Notebook)) { - return keybinding - ? nls.localize('notebookTreeAriaLabelHelp', "Notebook\nUse {0} for accessibility help", keybinding) - : nls.localize('notebookTreeAriaLabelHelpNoKb', "Notebook\nRun the Open Accessibility Help command for more information", keybinding); - } - return nls.localize('notebookTreeAriaLabel', "Notebook"); - }; + const accessibilityProvider = new NotebookAccessibilityProvider(this.notebookExecutionStateService, () => this.viewModel, this.keybindingService, this.configurationService); + this._register(accessibilityProvider); this._list = this.instantiationService.createInstance( NotebookCellList, @@ -950,21 +942,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD listInactiveFocusBackground: notebookEditorBackground, listInactiveFocusOutline: notebookEditorBackground, }, - accessibilityProvider: { - getAriaLabel: (element: CellViewModel) => { - if (!this.viewModel) { - return ''; - } - const index = this.viewModel.getCellIndex(element); - - if (index >= 0) { - return `Cell ${index}, ${element.cellKind === CellKind.Markup ? 'markdown' : 'code'} cell`; - } - - return ''; - }, - getWidgetAriaLabel: createNotebookAriaLabel - }, + accessibilityProvider }, ); this._dndController.setList(this._list); @@ -1025,9 +1003,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(); } })); @@ -1049,7 +1026,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Notebook)) { - this._list.ariaLabel = createNotebookAriaLabel(); + this._list.ariaLabel = accessibilityProvider?.getWidgetAriaLabel(); } })); } @@ -2005,6 +1982,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } + selectOutputContent(cell: ICellViewModel) { + this._webview?.selectOutputContents(cell); + } + + selectInputContents(cell: ICellViewModel) { + this._webview?.selectInputContents(cell); + } + onWillHide() { this._isVisible = false; this._editorFocus.set(false); @@ -2147,8 +2132,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,12 +2431,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } - const focusElementId = options?.outputId ?? cell.id; + const firstOutputId = cell.outputsViewModels.find(o => o.model.alternativeOutputId)?.model.alternativeOutputId; + const focusElementId = options?.outputId ?? firstOutputId ?? cell.id; this._webview.focusOutput(focusElementId, options?.altOutputId, options?.outputWebviewFocused || this._webviewFocused); cell.updateEditState(CellEditState.Preview, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Output; cell.focusedOutputId = options?.outputId; + this._outputFocus.set(true); if (!options?.skipReveal) { this.revealInCenterIfOutsideViewport(cell); } @@ -2471,7 +2458,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/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts index dbc9e88e9a4..52157f4ae29 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts index c2c7f3e740a..0c859ef8476 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts +++ b/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, @@ -57,25 +61,36 @@ export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { @IContextMenuService _contextMenuService: IContextMenuService, @IThemeService _themeService: IThemeService ) { - super(action, options, _keybindingService, _contextMenuService, _themeService); + super(action, { ...options, hoverDelegate: options?.hoverDelegate ?? getDefaultHoverDelegate('element') }, _keybindingService, _contextMenuService, _themeService); } 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); - const hoverDelegate = this.options.hoverDelegate ?? getDefaultHoverDelegate('element'); - this._hover = this._register(setupCustomHover(hoverDelegate, 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/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index bf0fe703109..1c42939cab4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index eb2c45f2f8a..fc8032f7853 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/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, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS } 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,8 @@ export class CellContextKeyManager extends Disposable { private cellOutputCollapsed!: IContextKey; private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; private cellResource!: IContextKey; + private cellGeneratedByChat!: IContextKey; + private cellHasErrorDiagnostics!: IContextKey; private markdownEditMode!: IContextKey; @@ -70,7 +73,9 @@ 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); + this.cellHasErrorDiagnostics = NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS.bindTo(this._contextKeyService); if (element) { this.updateForElement(element); @@ -96,6 +101,7 @@ export class CellContextKeyManager extends Disposable { if (element instanceof CodeCellViewModel) { this.elementDisposables.add(element.onDidChangeOutputs(() => this.updateForOutputs())); + this.elementDisposables.add(element.cellDiagnostics.onDidDiagnosticsChange(() => this.updateForDiagnostics())); } this.elementDisposables.add(this.notebookEditor.onDidChangeActiveCell(() => this.updateForFocusState())); @@ -112,10 +118,22 @@ export class CellContextKeyManager extends Disposable { this.updateForEditState(); this.updateForCollapseState(); this.updateForOutputs(); + this.updateForChat(); + this.updateForDiagnostics(); 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 +234,21 @@ 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)); + } + + private updateForDiagnostics() { + if (this.element instanceof CodeCellViewModel) { + this.cellHasErrorDiagnostics.set(!!this.element.cellDiagnostics.ErrorDetails); + } + } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index 52e87ad8086..88c4252fd82 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts index a7b79d85684..250cb9824ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts @@ -22,6 +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 { 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; @@ -44,12 +46,12 @@ export class BetweenCellToolbar extends CellOverlayPart { } const betweenCellToolbar = this._register(new ToolBar(this._bottomCellToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action instanceof MenuItemAction) { if (this._notebookEditor.notebookOptions.getDisplayOptions().insertToolbarAlignment === 'center') { - return this.instantiationService.createInstance(CodiconActionViewItem, action, undefined); + return this.instantiationService.createInstance(CodiconActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } else { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } } @@ -165,15 +167,16 @@ export class CellTitleToolbarPart extends CellOverlayPart { if (this._view) { return this._view; } - + const hoverDelegate = this._register(createInstantHoverDelegate()); const toolbar = this._register(this.instantiationService.createInstance(WorkbenchToolBar, this.toolbarContainer, { actionViewItemProvider: (action, options) => { return createActionViewItem(this.instantiationService, action, options); }, - renderDropdownAsChildElement: true + renderDropdownAsChildElement: true, + hoverDelegate })); - const deleteToolbar = this._register(this.instantiationService.invokeFunction(accessor => createDeleteToolbar(accessor, this.toolbarContainer, 'cell-delete-toolbar'))); + const deleteToolbar = this._register(this.instantiationService.invokeFunction(accessor => createDeleteToolbar(accessor, this.toolbarContainer, hoverDelegate, 'cell-delete-toolbar'))); if (model.deleteActions.primary.length !== 0 || model.deleteActions.secondary.length !== 0) { deleteToolbar.setActions(model.deleteActions.primary, model.deleteActions.secondary); } @@ -269,7 +272,7 @@ function getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IA return result; } -function createDeleteToolbar(accessor: ServicesAccessor, container: HTMLElement, elementClass?: string): ToolBar { +function createDeleteToolbar(accessor: ServicesAccessor, container: HTMLElement, hoverDelegate: IHoverDelegate, elementClass?: string): ToolBar { const contextMenuService = accessor.get(IContextMenuService); const keybindingService = accessor.get(IKeybindingService); const instantiationService = accessor.get(IInstantiationService); @@ -278,7 +281,8 @@ function createDeleteToolbar(accessor: ServicesAccessor, container: HTMLElement, actionViewItemProvider: (action, options) => { return createActionViewItem(instantiationService, action, options); }, - renderDropdownAsChildElement: true + renderDropdownAsChildElement: true, + hoverDelegate }); if (elementClass) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts index 48974ad10a5..de2c0e912bf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts @@ -84,7 +84,7 @@ export class RunToolbar extends CellContentPart { const executionContextKeyService = this._register(getCodeCellExecutionContextKeyService(contextKeyService)); this.toolbar = this._register(new ToolBar(container, this.contextMenuService, { getKeyBinding: keybindingProvider, - actionViewItemProvider: _action => { + actionViewItemProvider: (_action, _options) => { actionViewItemDisposables.clear(); const primary = this.getCellToolbarActions(this.primaryMenu).primary[0]; @@ -104,6 +104,7 @@ export class RunToolbar extends CellContentPart { 'notebook-cell-run-toolbar', this.contextMenuService, { + ..._options, getKeyBinding: keybindingProvider }); actionViewItemDisposables.add(item.onDidChangeDropdownVisibility(visible => { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts index 2fe72e05af8..211e85e9a62 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts index 5806dd97b5f..d334bb1db3f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts @@ -11,7 +11,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ILanguageService } from 'vs/editor/common/languages/language'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index bd47eb879e9..48910a20dba 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/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.cellKind === CellKind.Code) + ) && !cell.editorAttached) { return getEditorAttachedPromise(cell); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index e7f7d018a80..399934d7c49 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -295,8 +295,20 @@ export class NotebookCellListView extends ListView { } removeWhitespace(id: string): void { - this.notebookRangeMap.removeWhitespace(id); - this.eventuallyUpdateScrollDimensions(); + const scrollTop = this.scrollTop; + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const currentPosition = this.notebookRangeMap.getWhitespacePosition(id); + + if (currentPosition > scrollTop) { + this.notebookRangeMap.removeWhitespace(id); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); + } else { + this.notebookRangeMap.removeWhitespace(id); + this.eventuallyUpdateScrollDimensions(); + } + } getWhitespacePosition(id: string): number { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index dd421641fb8..315c68a31e0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/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(); @@ -678,6 +686,7 @@ export class BackLayerWebView extends Themable { const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); if (latestCell) { latestCell.outputIsFocused = false; + latestCell.inputInOutputIsFocused = false; } } break; @@ -903,6 +912,13 @@ export class BackLayerWebView extends Themable { break; } case 'outputInputFocus': { + const resolvedResult = this.resolveOutputId(data.id); + if (resolvedResult) { + const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); + if (latestCell) { + latestCell.inputInOutputIsFocused = data.inputFocused; + } + } this.notebookEditor.didFocusOutputInputChange(data.inputFocused); } } @@ -1123,7 +1139,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 +1155,7 @@ export class BackLayerWebView extends Themable { localResourceRoots: this.localResourceRootsCache, }, extension: undefined, - providedViewType: 'notebook.output', - codeWindow: codeWindow + providedViewType: 'notebook.output' }); webview.setHtml(content); @@ -1674,6 +1689,30 @@ 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 + }); + } + + selectInputContents(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-input-contents', + cellOrOutputId: outputId || cell.id + }); + } + focusOutput(cellOrOutputId: string, alternateId: string | undefined, viewFocused: boolean) { if (this._disposed) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 69133337125..9f6f5b8dac5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -9,7 +9,7 @@ import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -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/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index b46964be307..ab8ff3a55f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -50,6 +50,7 @@ export interface IOutputBlurMessage extends BaseToWebviewMessage { export interface IOutputInputFocusMessage extends BaseToWebviewMessage { readonly type: 'outputInputFocus'; readonly inputFocused: boolean; + readonly id: string; } export interface IScrollToRevealMessage extends BaseToWebviewMessage { @@ -168,23 +169,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 +460,15 @@ export interface IReturnOutputItemMessage { readonly output: OutputItemEntry | undefined; } +export interface ISelectOutputItemMessage { + readonly type: 'select-output-contents'; + readonly cellOrOutputId: string; +} +export interface ISelectInputOutputItemMessage { + readonly type: 'select-input-contents'; + readonly cellOrOutputId: string; +} + export interface ILogRendererDebugMessage extends BaseToWebviewMessage { readonly type: 'logRendererDebugMessage'; readonly message: string; @@ -555,7 +548,9 @@ export type ToWebviewMessage = IClearMessage | IFindHighlightCurrentMessage | IFindUnHighlightCurrentMessage | IFindStopMessage | - IReturnOutputItemMessage; + IReturnOutputItemMessage | + ISelectOutputItemMessage | + ISelectInputOutputItemMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index c08eb70c296..ee90f979a66 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -159,20 +159,47 @@ 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; } - if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') { - postNotebookMessage('outputInputFocus', { inputFocused: true }); + const id = lastFocusedOutput?.id; + if (id && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { + postNotebookMessage('outputInputFocus', { inputFocused: true, id }); activeElement.addEventListener('blur', () => { - postNotebookMessage('outputInputFocus', { inputFocused: false }); + postNotebookMessage('outputInputFocus', { inputFocused: false, id }); }, { once: true }); } }; @@ -182,16 +209,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 +271,85 @@ 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 selectInputContents = (cellOrOutputId: string) => { + const cellOutputContainer = window.document.getElementById(cellOrOutputId); + if (!cellOutputContainer) { + return; + } + const activeElement = window.document.activeElement; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') { + (activeElement as HTMLInputElement).select(); + } + }; + + 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; + } + const activeElement = window.document.activeElement; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') { + // Leave for default behavior. + 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 disableNativeSelectAll = (e: KeyboardEvent) => { + if (!lastFocusedOutput?.id) { + return; + } + const activeElement = window.document.activeElement; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') { + // The input element will handle this. + return; + } + + if ((e.key === 'a' && e.ctrlKey) || (e.metaKey && e.key === 'a')) { + e.preventDefault(); // We will handle selection in editor code. + return; + } + }; const handleDataUrl = async (data: string | ArrayBuffer | null, downloadName: string) => { postNotebookMessage('clicked-data-url', { @@ -277,6 +374,9 @@ 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); + window.document.body.addEventListener('keydown', disableNativeSelectAll); interface RendererContext extends rendererApi.RendererContext { readonly onDidChangeSettings: Event; @@ -455,7 +555,52 @@ async function webviewPreloads(ctx: PreloadContext) { } }; - function scrollWillGoToParent(event: WheelEvent) { + let previousDelta: number | undefined; + let scrollTimeout: any /* NodeJS.Timeout */ | undefined; + let scrolledElement: Element | undefined; + let lastTimeScrolled: number | undefined; + function flagRecentlyScrolled(node: Element, deltaY?: number) { + scrolledElement = node; + if (deltaY === undefined) { + lastTimeScrolled = Date.now(); + previousDelta = undefined; + node.setAttribute('recentlyScrolled', 'true'); + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300); + return true; + } + + if (node.hasAttribute('recentlyScrolled')) { + if (lastTimeScrolled && Date.now() - lastTimeScrolled > 300) { + // it has been a while since we actually scrolled + // if scroll velocity increases, it's likely a new scroll event + if (!!previousDelta && deltaY < 0 && deltaY < previousDelta - 2) { + clearTimeout(scrollTimeout); + scrolledElement?.removeAttribute('recentlyScrolled'); + return false; + } else if (!!previousDelta && deltaY > 0 && deltaY > previousDelta + 2) { + clearTimeout(scrollTimeout); + scrolledElement?.removeAttribute('recentlyScrolled'); + return false; + } + + // the tail end of a smooth scrolling event (from a trackpad) can go on for a while + // so keep swallowing it, but we can shorten the timeout since the events occur rapidly + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 50); + } else { + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300); + } + + previousDelta = deltaY; + return true; + } + + return false; + } + + 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; @@ -464,6 +609,7 @@ async function webviewPreloads(ctx: PreloadContext) { // scroll up if (event.deltaY < 0 && node.scrollTop > 0) { // there is still some content to scroll + flagRecentlyScrolled(node); return true; } @@ -481,6 +627,11 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } + flagRecentlyScrolled(node); + return true; + } + + if (flagRecentlyScrolled(node, event.deltaY)) { return true; } } @@ -489,7 +640,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', { @@ -516,13 +667,19 @@ async function webviewPreloads(ctx: PreloadContext) { if (cellOutputContainer.contains(window.document.activeElement)) { return; } - + const id = cellOutputContainer.id; let focusableElement = cellOutputContainer.querySelector('[tabindex="0"], [href], button, input, option, select, textarea') as HTMLElement | null; if (!focusableElement) { focusableElement = cellOutputContainer; focusableElement.tabIndex = -1; + postNotebookMessage('outputInputFocus', { inputFocused: false, id }); + } else { + const inputFocused = focusableElement.tagName === 'INPUT' || focusableElement.tagName === 'TEXTAREA'; + postNotebookMessage('outputInputFocus', { inputFocused, id }); } + lastFocusedOutput = cellOutputContainer; + postNotebookMessage('outputFocus', { id: cellOutputContainer.id }); focusableElement.focus(); } } @@ -1575,6 +1732,12 @@ 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 'select-input-contents': + selectInputContents(event.data.cellOrOutputId); + break; case 'decorations': { let outputContainer = window.document.getElementById(event.data.cellId); if (!outputContainer) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 548a5a7c043..dd136ac471b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, IReference, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -19,11 +19,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IWordWrapTransientState, readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { CellEditState, CellFocusMode, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; export abstract class BaseCellViewModel extends Disposable { @@ -103,6 +103,7 @@ export abstract class BaseCellViewModel extends Disposable { private _editorViewStates: editorCommon.ICodeEditorViewState | null = null; private _editorTransientState: IWordWrapTransientState | null = null; private _resolvedCellDecorations = new Map(); + private _textModelRefChangeDisposable = this._register(new MutableDisposable()); private readonly _cellDecorationsChanged = this._register(new Emitter<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>()); onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }> = this._cellDecorationsChanged.event; @@ -299,6 +300,7 @@ export abstract class BaseCellViewModel extends Disposable { this._textModelRef.dispose(); this._textModelRef = undefined; } + this._textModelRefChangeDisposable.clear(); } getText(): string { @@ -618,8 +620,7 @@ export abstract class BaseCellViewModel extends Disposable { if (!this._textModelRef) { throw new Error(`Cannot resolve text model for ${this.uri}`); } - - this._register(this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent())); + this._textModelRefChangeDisposable.value = this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent()); } return this.textModel!; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts index cc3aa9cbca9..6c73c4348ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -22,7 +22,7 @@ export interface IViewCellEditingDelegate extends ITextCellEditingDelegate { export class JoinCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = 'Join Cell'; - code: string = 'undoredo.notebooks.joinCell'; + code: string = 'undoredo.textBufferEdit'; private _deletedRawCell: NotebookCellTextModel; constructor( public resource: URI, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 93ae763c6e9..da9b53fff5f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -22,6 +22,8 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookS import { BaseCellViewModel } from './baseCellViewModel'; import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { ICellExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { CellDiagnostics } from 'vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export const outputDisplayLimit = 500; @@ -44,6 +46,11 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod private _outputCollection: number[] = []; + private readonly _cellDiagnostics: CellDiagnostics; + get cellDiagnostics() { + return this._cellDiagnostics; + } + private _outputsTop: PrefixSumComputer | null = null; protected _pauseableEmitter = this._register(new PauseableEmitter()); @@ -108,6 +115,15 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._onDidChangeState.fire({ outputIsFocusedChanged: true }); } + private _focusInputInOutput: boolean = false; + public get inputInOutputIsFocused(): boolean { + return this._focusInputInOutput; + } + + public set inputInOutputIsFocused(v: boolean) { + this._focusInputInOutput = v; + } + private _outputMinHeight: number = 0; private get outputMinHeight() { @@ -143,7 +159,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod @INotebookService private readonly _notebookService: INotebookService, @ITextModelService modelService: ITextModelService, @IUndoRedoService undoRedoService: IUndoRedoService, - @ICodeEditorService codeEditorService: ICodeEditorService + @ICodeEditorService codeEditorService: ICodeEditorService, + @IInstantiationService instantiationService: IInstantiationService ) { super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService, codeEditorService); this._outputViewModels = this.model.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService)); @@ -166,11 +183,17 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod if (outputLayoutChange) { this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs'); } + if (this._outputCollection.length === 0) { + this._cellDiagnostics.clear(); + } dispose(removedOutputs); })); this._outputCollection = new Array(this.model.outputs.length); + this._cellDiagnostics = instantiationService.createInstance(CellDiagnostics, this); + this._register(this._cellDiagnostics); + this._layoutInfo = { fontInfo: initialNotebookLayoutInfo?.fontInfo || null, editorHeight: 0, @@ -425,6 +448,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent'); this._onDidChangeState.fire({ contentChanged: true }); } + this._cellDiagnostics.clear(); } onDeselect() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts index 143d969f6ba..4b86fdf8427 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts @@ -317,7 +317,7 @@ export function* getMarkdownHeadersInCell(cellContent: string): Iterable<{ reado if (token.type === 'heading') { yield { depth: token.depth, - text: renderMarkdownAsPlaintext({ value: token.text }).trim() + text: renderMarkdownAsPlaintext({ value: token.raw }).trim() }; } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 41fbef9a007..0f255cc20db 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -93,6 +93,14 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM this._focusOnOutput = v; } + public get inputInOutputIsFocused(): boolean { + return false; + } + + public set inputInOutputIsFocused(_: boolean) { + // + } + private _hoveringCell = false; public get cellIsHovered(): boolean { return this._hoveringCell; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts index 54335576ac9..0222b835bda 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts @@ -14,6 +14,7 @@ import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { IRange } from 'vs/editor/common/core/range'; import { SymbolKind } from 'vs/editor/common/languages'; +import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; type entryDesc = { name: string; @@ -30,7 +31,7 @@ export class NotebookOutlineEntryFactory { private readonly executionStateService: INotebookExecutionStateService ) { } - public getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[] { + public getOutlineEntries(cell: ICellViewModel, target: OutlineTarget, index: number): OutlineEntry[] { const entries: OutlineEntry[] = []; const isMarkdown = cell.cellKind === CellKind.Markup; @@ -65,26 +66,30 @@ export class NotebookOutlineEntryFactory { } if (!hasHeader) { + const exeState = !isMarkdown && this.executionStateService.getCellExecution(cell.uri); + let preview = content.trim(); + if (!isMarkdown && cell.model.textModel) { const cachedEntries = this.cellOutlineEntryCache[cell.model.textModel.id]; // Gathering symbols from the model is an async operation, but this provider is syncronous. // So symbols need to be precached before this function is called to get the full list. if (cachedEntries) { + // push code cell that is a parent of cached symbols if we are targeting the outlinePane + if (target === OutlineTarget.OutlinePane) { + entries.push(new OutlineEntry(index++, 7, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); + } cachedEntries.forEach((cached) => { entries.push(new OutlineEntry(index++, cached.level, cell, cached.name, false, false, cached.range, cached.kind)); }); } } - const exeState = !isMarkdown && this.executionStateService.getCellExecution(cell.uri); - if (entries.length === 0) { - let preview = content.trim(); + if (entries.length === 0) { // if there are no cached entries, use the first line of the cell as a code cell if (preview.length === 0) { // empty or just whitespace preview = localize('empty', "empty cell"); } - entries.push(new OutlineEntry(index++, 7, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); } } @@ -95,7 +100,7 @@ export class NotebookOutlineEntryFactory { public async cacheSymbols(cell: ICellViewModel, outlineModelService: IOutlineModelService, cancelToken: CancellationToken) { const textModel = await cell.resolveTextModel(); const outlineModel = await outlineModelService.getOrCreate(textModel, cancelToken); - const entries = createOutlineEntries(outlineModel.getTopLevelSymbols(), 7); + const entries = createOutlineEntries(outlineModel.getTopLevelSymbols(), 8); this.cellOutlineEntryCache[textModel.id] = entries; } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts index bdb16299dc1..fb5c32bb37c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts @@ -10,8 +10,8 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IActiveNotebookEditor, INotebookEditor, INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IActiveNotebookEditor, ICellViewModel, INotebookEditor, INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; import { OutlineEntry } from './OutlineEntry'; @@ -70,7 +70,11 @@ export class NotebookCellOutlineProvider { ); this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.outline.showCodeCells')) { + if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly) || + e.affectsConfiguration(NotebookSetting.outlineShowCodeCells) || + e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols) || + e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells) + ) { this._recomputeState(); } })); @@ -136,17 +140,20 @@ export class NotebookCellOutlineProvider { } let includeCodeCells = true; - if (this._target === OutlineTarget.OutlinePane) { - includeCodeCells = this._configurationService.getValue('notebook.outline.showCodeCells'); - } else if (this._target === OutlineTarget.Breadcrumbs) { + if (this._target === OutlineTarget.Breadcrumbs) { includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); } - const notebookCells = notebookEditorWidget.getViewModel().viewCells.filter((cell) => cell.cellKind === CellKind.Markup || includeCodeCells); + let notebookCells: ICellViewModel[]; + if (this._target === OutlineTarget.Breadcrumbs) { + notebookCells = notebookEditorWidget.getViewModel().viewCells.filter((cell) => cell.cellKind === CellKind.Markup || includeCodeCells); + } else { + notebookCells = notebookEditorWidget.getViewModel().viewCells; + } const entries: OutlineEntry[] = []; for (const cell of notebookCells) { - entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, entries.length)); + entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, this._target, entries.length)); // send an event whenever any of the cells change this._entriesDisposables.add(cell.model.onDidChangeContent(() => { this._recomputeState(); @@ -262,8 +269,6 @@ export class NotebookCellOutlineProvider { } } - - get isEmpty(): boolean { return this._entries.length === 0; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 6444aa95c45..1e17428ab58 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index a02e1bb2010..5900460feb9 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/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); } @@ -313,7 +281,7 @@ export class NotebookStickyScroll extends Disposable { static computeStickyHeight(entry: OutlineEntry) { let height = 0; - if (entry.cell.cellKind === CellKind.Markup && entry.level !== 7) { + if (entry.cell.cellKind === CellKind.Markup && entry.level < 7) { height += 22; } while (entry.parent) { @@ -329,8 +297,8 @@ export class NotebookStickyScroll extends Disposable { const elementsToRender = []; while (currentEntry) { - if (currentEntry.level === 7) { - // level 7 represents a non-header entry, which we don't want to render + if (currentEntry.level >= 7) { + // level 7+ represents a non-header entry, which we don't want to render currentEntry = currentEntry.parent; continue; } @@ -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); } @@ -413,7 +382,7 @@ export function computeContent(notebookEditor: INotebookEditor, notebookCellList if (visibleRange.start === 0) { const firstCell = notebookEditor.cellAt(0); const firstCellEntry = NotebookStickyScroll.getVisibleOutlineEntry(0, notebookOutlineEntries); - if (firstCell && firstCellEntry && firstCell.cellKind === CellKind.Markup && firstCellEntry.level !== 7) { + if (firstCell && firstCellEntry && firstCell.cellKind === CellKind.Markup && firstCellEntry.level < 7) { if (notebookEditor.scrollTop > 22) { const newMap = NotebookStickyScroll.checkCollapsedStickyLines(firstCellEntry, 100, notebookEditor); return newMap; @@ -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,11 +414,11 @@ 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) --------------------- - if (nextCell.cellKind === CellKind.Markup && nextCellEntry.level !== 7) { + if (nextCell.cellKind === CellKind.Markup && nextCellEntry.level < 7) { const sectionBottom = notebookCellList.getCellViewScrollTop(nextCell); const currentSectionStickyHeight = NotebookStickyScroll.computeStickyHeight(cellEntry); const nextSectionStickyHeight = NotebookStickyScroll.computeStickyHeight(nextCellEntry); @@ -488,5 +457,3 @@ export function computeContent(notebookEditor: INotebookEditor, notebookCellList const newMap = NotebookStickyScroll.checkCollapsedStickyLines(cellEntry, linesToRender, notebookEditor); return newMap; } - -registerAction2(ToggleNotebookStickyScroll); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index e2972e82e8a..969127bc917 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -28,6 +28,8 @@ import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookO import { IActionViewItem, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { disposableTimeout } from 'vs/base/common/async'; import { HiddenItemStrategy, IWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; interface IActionModel { action: IAction; @@ -75,18 +77,18 @@ class WorkbenchAlwaysLabelStrategy implements IActionLayoutStrategy { readonly goToMenu: IMenu, readonly instantiationService: IInstantiationService) { } - actionProvider(action: IAction): IActionViewItem | undefined { + actionProvider(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { if (action.id === SELECT_KERNEL_ID) { // this is being disposed by the consumer - return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor); + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor, options); } if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ActionViewWithLabel, action, undefined); + return this.instantiationService.createInstance(ActionViewWithLabel, action, { hoverDelegate: options.hoverDelegate }); } if (action instanceof SubmenuItemAction && action.item.submenu.id === MenuId.NotebookCellExecuteGoTo.id) { - return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, undefined, true, { + return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, { hoverDelegate: options.hoverDelegate }, true, { getActions: () => { return this.goToMenu.getActions().find(([group]) => group === 'navigation/execute')?.[1] ?? []; } @@ -115,25 +117,25 @@ class WorkbenchNeverLabelStrategy implements IActionLayoutStrategy { readonly goToMenu: IMenu, readonly instantiationService: IInstantiationService) { } - actionProvider(action: IAction): IActionViewItem | undefined { + actionProvider(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { if (action.id === SELECT_KERNEL_ID) { // this is being disposed by the consumer - return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor); + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor, options); } if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); + return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } if (action instanceof SubmenuItemAction) { if (action.item.submenu.id === MenuId.NotebookCellExecuteGoTo.id) { - return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, undefined, false, { + return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, { hoverDelegate: options.hoverDelegate }, false, { getActions: () => { return this.goToMenu.getActions().find(([group]) => group === 'navigation/execute')?.[1] ?? []; } }, this.actionProvider.bind(this)); } else { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, undefined); + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } } @@ -159,20 +161,20 @@ class WorkbenchDynamicLabelStrategy implements IActionLayoutStrategy { readonly goToMenu: IMenu, readonly instantiationService: IInstantiationService) { } - actionProvider(action: IAction): IActionViewItem | undefined { + actionProvider(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { if (action.id === SELECT_KERNEL_ID) { // this is being disposed by the consumer - return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor); + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor, options); } const a = this.editorToolbar.primaryActions.find(a => a.action.id === action.id); if (!a || a.renderLabel) { if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ActionViewWithLabel, action, undefined); + return this.instantiationService.createInstance(ActionViewWithLabel, action, { hoverDelegate: options.hoverDelegate }); } if (action instanceof SubmenuItemAction && action.item.submenu.id === MenuId.NotebookCellExecuteGoTo.id) { - return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, undefined, true, { + return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, { hoverDelegate: options.hoverDelegate }, true, { getActions: () => { return this.goToMenu.getActions().find(([group]) => group === 'navigation/execute')?.[1] ?? []; } @@ -182,18 +184,18 @@ class WorkbenchDynamicLabelStrategy implements IActionLayoutStrategy { return undefined; } else { if (action instanceof MenuItemAction) { - this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); + this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } if (action instanceof SubmenuItemAction) { if (action.item.submenu.id === MenuId.NotebookCellExecuteGoTo.id) { - return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, undefined, false, { + return this.instantiationService.createInstance(UnifiedSubmenuActionView, action, { hoverDelegate: options.hoverDelegate }, false, { getActions: () => { return this.goToMenu.getActions().find(([group]) => group === 'navigation/execute')?.[1] ?? []; } }, this.actionProvider.bind(this)); } else { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, undefined); + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }); } } @@ -321,24 +323,29 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { notebookEditor: this.notebookEditor }; - const actionProvider = (action: IAction) => { + const actionProvider = (action: IAction, options: IActionViewItemOptions) => { if (action.id === SELECT_KERNEL_ID) { // this is being disposed by the consumer - return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor); + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor, options); } if (this._renderLabel !== RenderLabel.Never) { const a = this._primaryActions.find(a => a.action.id === action.id); if (a && a.renderLabel) { - return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action, undefined) : undefined; + return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action, { hoverDelegate: options.hoverDelegate }) : undefined; } else { - return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined; + return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) : undefined; } } else { - return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined; + return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) : undefined; } }; + // Make sure both toolbars have the same hover delegate for instant hover to work + // Due to the elements being further apart than normal toolbars, the default time limit is to short and has to be increased + const hoverDelegate = this._register(this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', true, {})); + hoverDelegate.setInstantHoverTimeLimit(600); + const leftToolbarOptions: IWorkbenchToolBarOptions = { hiddenItemStrategy: HiddenItemStrategy.RenderInSecondaryGroup, resetMenu: MenuId.NotebookToolbar, @@ -347,6 +354,7 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { }, getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), renderDropdownAsChildElement: true, + hoverDelegate }; this._notebookLeftToolbar = this.instantiationService.createInstance( @@ -363,7 +371,8 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { this._notebookRightToolbar = new ToolBar(this._notebookTopRightToolbarContainer, this.contextMenuService, { getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), actionViewItemProvider: actionProvider, - renderDropdownAsChildElement: true + renderDropdownAsChildElement: true, + hoverDelegate }); this._register(this._notebookRightToolbar); this._notebookRightToolbar.context = context; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 9633771a01e..3b62e7611e8 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -406,7 +406,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/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts index 34958f1e7c0..8c3ecd82436 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action, IAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { localize, localize2 } from 'vs/nls'; @@ -136,13 +136,14 @@ export class NotebooKernelActionViewItem extends ActionViewItem { constructor( actualAction: IAction, private readonly _editor: { onDidChangeModel: Event; textModel: NotebookTextModel | undefined; scopedContextKeyService?: IContextKeyService } | INotebookEditor, + options: IActionViewItemOptions, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @INotebookKernelHistoryService private readonly _notebookKernelHistoryService: INotebookKernelHistoryService, ) { super( undefined, new Action('fakeAction', undefined, ThemeIcon.asClassName(selectKernelIcon), true, (event) => actualAction.run(event)), - { label: false, icon: true } + { ...options, label: false, icon: true } ); this._register(_editor.onDidChangeModel(this._update, this)); this._register(_notebookKernelService.onDidAddKernel(this._update, this)); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts index 9a558d0dd28..f606649ca03 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts @@ -96,9 +96,9 @@ export class ListTopCellToolbar extends Disposable { DOM.clearNode(this.topCellToolbar); const toolbar = this.instantiationService.createInstance(MenuWorkbenchToolBar, this.topCellToolbar, this.notebookEditor.creationOptions.menuIds.cellTopInsertToolbar, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action instanceof MenuItemAction) { - const item = this.instantiationService.createInstance(CodiconActionViewItem, action, undefined); + const item = this.instantiationService.createInstance(CodiconActionViewItem, action, { hoverDelegate: options.hoverDelegate }); return item; } diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index 3291110ac93..0625af50b5d 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -21,8 +21,10 @@ export interface ITextCellEditingDelegate { export class MoveCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Move Cell'; - code: string = 'undoredo.notebooks.moveCell'; + get label() { + return this.length === 1 ? 'Move Cell' : 'Move Cells'; + } + code: string = 'undoredo.textBufferEdit'; constructor( public resource: URI, @@ -54,8 +56,18 @@ export class MoveCellEdit implements IResourceUndoRedoElement { export class SpliceCellsEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - label: string = 'Insert Cell'; - code: string = 'undoredo.notebooks.insertCell'; + 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.textBufferEdit'; constructor( public resource: URI, private diffs: [number, NotebookCellTextModel[], NotebookCellTextModel[]][], @@ -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/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 2f72b627840..3f57341e228 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/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; } @@ -1026,14 +1026,14 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return that.uri; } readonly label = 'Update Cell Language'; - readonly code = 'undoredo.notebooks.updateCellLanguage'; + readonly code = 'undoredo.textBufferEdit'; 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/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c673ae8ad79..07090093a0c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/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 { @@ -945,10 +947,16 @@ export const NotebookSetting = { confirmDeleteRunningCell: 'notebook.confirmDeleteRunningCell', remoteSaving: 'notebook.experimental.remoteSave', gotoSymbolsAllSymbols: 'notebook.gotoSymbols.showAllSymbols', + outlineShowMarkdownHeadersOnly: 'notebook.outline.showMarkdownHeadersOnly', + outlineShowCodeCells: 'notebook.outline.showCodeCells', + outlineShowCodeCellSymbols: 'notebook.outline.showCodeCellSymbols', + breadcrumbsShowCodeCells: 'notebook.breadcrumbs.showCodeCells', scrollToRevealCell: 'notebook.scrolling.revealNextCellOnExecute', anchorToFocusedCell: 'notebook.scrolling.experimental.anchorToFocusedCell', cellChat: 'notebook.experimental.cellChat', - notebookVariablesView: 'notebook.experimental.variablesView' + notebookVariablesView: 'notebook.experimental.variablesView', + InteractiveWindowPromptToSave: 'interactiveWindow.promptToSaveOnClose', + cellFailureDiagnostics: 'notebook.cellFailureDiagnostics', } as const; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 42b5d294c13..a487c2c0fda 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -46,6 +46,8 @@ 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); +export const NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS = new RawContextKey('notebookCellHasErrorDiagnostics', false); // Kernels export const NOTEBOOK_KERNEL = new RawContextKey('notebookKernel', undefined); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 7c1a89da760..0ac6de71100 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -30,6 +30,7 @@ import { localize } from 'vs/nls'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; export interface NotebookEditorInputOptions { startDirty?: boolean; @@ -42,23 +43,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; } @@ -81,9 +70,10 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IExtensionService extensionService: IExtensionService, @IEditorService editorService: IEditorService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService ) { - super(resource, preferredResource, labelService, fileService, filesConfigurationService, textResourceConfigurationService); + super(resource, preferredResource, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); this._defaultDirtyState = !!options.startDirty; // Automatically resolve this input when the "wanted" model comes to life via diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index f54d5f73092..e91e96ece99 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/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/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index bbc69d31aa2..0a8a17a170d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/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/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts index 98a24d28adf..5b98e7ca262 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts +++ b/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/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts index 8826eb3dda7..fdb19d20242 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts @@ -12,6 +12,7 @@ import { IOutlineModelService, OutlineModel } from 'vs/editor/contrib/documentSy import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; suite('Notebook Symbols', function () { ensureNoDisposablesAreLeakedInTestSuite(); @@ -66,7 +67,7 @@ suite('Notebook Symbols', function () { test('Cell without symbols cache', function () { setSymbolsForTextModel([{ name: 'var', range: {} }]); const entryFactory = new NotebookOutlineEntryFactory(executionService); - const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), OutlineTarget.QuickPick, 0); assert.equal(entries.length, 1, 'no entries created'); assert.equal(entries[0].label, '# code', 'entry should fall back to first line of cell'); @@ -78,15 +79,15 @@ suite('Notebook Symbols', function () { const cell = createCellViewModel(); await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); - const entries = entryFactory.getOutlineEntries(cell, 0); + const entries = entryFactory.getOutlineEntries(cell, OutlineTarget.QuickPick, 0); assert.equal(entries.length, 2, 'wrong number of outline entries'); assert.equal(entries[0].label, 'var1'); // 6 levels for markdown, all code symbols are greater than the max markdown level - assert.equal(entries[0].level, 7); + assert.equal(entries[0].level, 8); assert.equal(entries[0].index, 0); assert.equal(entries[1].label, 'var2'); - assert.equal(entries[1].level, 7); + assert.equal(entries[1].level, 8); assert.equal(entries[1].index, 1); }); @@ -99,19 +100,19 @@ suite('Notebook Symbols', function () { const cell = createCellViewModel(); await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); - const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), OutlineTarget.QuickPick, 0); assert.equal(entries.length, 5, 'wrong number of outline entries'); assert.equal(entries[0].label, 'root1'); - assert.equal(entries[0].level, 7); + assert.equal(entries[0].level, 8); assert.equal(entries[1].label, 'nested1'); - assert.equal(entries[1].level, 8); + assert.equal(entries[1].level, 9); assert.equal(entries[2].label, 'nested2'); - assert.equal(entries[2].level, 8); + assert.equal(entries[2].level, 9); assert.equal(entries[3].label, 'root2'); - assert.equal(entries[3].level, 7); + assert.equal(entries[3].level, 8); assert.equal(entries[4].label, 'nested1'); - assert.equal(entries[4].level, 8); + assert.equal(entries[4].level, 9); }); test('Multiple Cells with symbols', async function () { @@ -124,8 +125,8 @@ suite('Notebook Symbols', function () { await entryFactory.cacheSymbols(cell1, outlineModelService, CancellationToken.None); await entryFactory.cacheSymbols(cell2, outlineModelService, CancellationToken.None); - const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), 0); - const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), 0); + const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), OutlineTarget.QuickPick, 0); + const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), OutlineTarget.QuickPick, 0); assert.equal(entries1.length, 1, 'wrong number of outline entries'); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index 6136451c28a..654fe7ee807 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/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/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts index 8de6a93e27e..fa082d78e23 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts +++ b/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/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index dbcfcbe1a41..228e0bb1fdf 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -65,6 +65,8 @@ import { EditorFontLigatures, EditorFontVariations } from 'vs/editor/common/conf import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { mainWindow } from 'vs/base/browser/window'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; +import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; export class TestCell extends NotebookCellTextModel { constructor( @@ -197,7 +199,7 @@ export function setupInstantiationService(disposables: DisposableStore) { instantiationService.stub(IKeybindingService, new MockKeybindingService()); instantiationService.stub(INotebookCellStatusBarService, disposables.add(new NotebookCellStatusBarService())); instantiationService.stub(ICodeEditorService, disposables.add(new TestCodeEditorService(testThemeService))); - + instantiationService.stub(IInlineChatService, instantiationService.createInstance(InlineChatServiceImpl)); return instantiationService; } diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 6756a2dd0fe..d00db5854fb 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/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/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 7dfb3118152..c71b70023e5 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/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); @@ -101,6 +103,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerOpenActiveOutputFileInAuxWindowAction(); this.registerShowLogsAction(); this.registerOpenLogFileAction(); + this.registerConfigureActiveOutputLogLevelAction(); } private registerSwitchOutputAction(): void { @@ -336,6 +339,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/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 26fed6ffc4e..eb83c902631 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/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/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index b7484919efa..53206dacd2e 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -33,6 +33,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; export class OutputViewPane extends ViewPane { @@ -159,10 +160,9 @@ class OutputEditor extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, - @IFileService fileService: IFileService, - @IContextKeyService contextKeyService: IContextKeyService, + @IFileService fileService: IFileService ) { - super(OUTPUT_VIEW_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* TODO@bpasero this is wrong */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey)); } @@ -213,6 +213,10 @@ class OutputEditor extends AbstractTextResourceEditor { return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel"); } + protected override computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel(); + } + override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (this.input && input.matches(this.input)) { diff --git a/src/vs/workbench/contrib/output/common/outputLinkComputer.ts b/src/vs/workbench/contrib/output/common/outputLinkComputer.ts index efab6b06075..0de2f9f2e15 100644 --- a/src/vs/workbench/contrib/output/common/outputLinkComputer.ts +++ b/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/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts index 1c92342f5bb..2f7988f5236 100644 --- a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts +++ b/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/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts index 2ea34f84b37..72451c4fdd7 100644 --- a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts +++ b/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/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index 259ce2da160..10577676318 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -30,6 +30,7 @@ import * as perf from 'vs/base/common/performance'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, getWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; export class PerfviewContrib { @@ -77,7 +78,8 @@ export class PerfviewInput extends TextResourceEditorInput { @IFileService fileService: IFileService, @ILabelService labelService: ILabelService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService ) { super( PerfviewContrib.get().getInputUri(), @@ -91,7 +93,8 @@ export class PerfviewInput extends TextResourceEditorInput { fileService, labelService, filesConfigurationService, - textResourceConfigurationService + textResourceConfigurationService, + customEditorLabelService ); } } diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx index a92fb3974cc..94d9aa03fda 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx @@ -17,7 +17,6 @@ import { ISelection } from 'vs/editor/common/core/selection'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { useStateRef } from 'vs/base/browser/ui/react/useStateRef'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { HoverController } from 'vs/editor/contrib/hover/browser/hover'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { MarkerController } from 'vs/editor/contrib/gotoError/browser/gotoError'; @@ -37,6 +36,7 @@ import { HistoryBrowserPopup } from 'vs/workbench/contrib/positronConsole/browse import { EmptyHistoryMatchStrategy, HistoryMatch, HistoryMatchStrategy } from 'vs/workbench/contrib/positronConsole/common/historyMatchStrategy'; import { HistoryPrefixMatchStrategy } from 'vs/workbench/contrib/positronConsole/common/historyPrefixMatchStrategy'; import { HistoryInfixMatchStrategy } from 'vs/workbench/contrib/positronConsole/common/historyInfixMatchStrategy'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; // Position enumeration. const enum Position { diff --git a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx index 16a5c335eb6..42e7d536991 100644 --- a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx +++ b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx @@ -171,6 +171,7 @@ export class PositronDataExplorerEditor extends EditorPane implements IReactComp /** * Constructor. + * @param _group The editor group. * @param _clipboardService The clipboard service. * @param _commandService The command service. * @param _configurationService The configuration service. @@ -183,6 +184,7 @@ export class PositronDataExplorerEditor extends EditorPane implements IReactComp * @param themeService The theme service. */ constructor( + readonly _group: IEditorGroup, @IClipboardService readonly _clipboardService: IClipboardService, @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -196,7 +198,7 @@ export class PositronDataExplorerEditor extends EditorPane implements IReactComp @IThemeService themeService: IThemeService, ) { // Call the base class's constructor. - super(PositronDataExplorerEditorInput.EditorID, telemetryService, themeService, storageService); + super(PositronDataExplorerEditorInput.EditorID, _group, telemetryService, themeService, storageService); // Logging. console.log(`PositronDataExplorerEditor ${this._instance} created`); @@ -311,14 +313,13 @@ export class PositronDataExplorerEditor extends EditorPane implements IReactComp /** * Sets editor visibility. * @param visible A value which indicates whether the editor should be visible. - * @param group The editor group. */ - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { // Logging. - console.log(`PositronDataExplorerEditor ${this._instance} setEditorVisible ${visible} group ${group?.id}`); + console.log(`PositronDataExplorerEditor ${this._instance} setEditorVisible ${visible} group ${this._group?.id}`); // Call the base class's method. - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } //#endregion Protected Overrides diff --git a/src/vs/workbench/contrib/positronHelp/browser/helpEntry.ts b/src/vs/workbench/contrib/positronHelp/browser/helpEntry.ts index 44320d87230..c1d0a1cb331 100644 --- a/src/vs/workbench/contrib/positronHelp/browser/helpEntry.ts +++ b/src/vs/workbench/contrib/positronHelp/browser/helpEntry.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2022 Posit Software, PBC. All rights reserved. + * Copyright (C) 2022-2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; @@ -558,7 +558,7 @@ export class HelpEntry extends Disposable implements IHelpEntry, WebviewFindDele // Claim and layout the help overlay webview. this._element = element; - this._helpOverlayWebview.claim(this._element, undefined); + this._helpOverlayWebview.claim(this._element, DOM.getWindow(element), undefined); this._helpOverlayWebview.layoutWebviewOverElement(this._element); } diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx index c648287c584..f4be209ebe9 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx @@ -46,6 +46,7 @@ import { PositronNotebookComponent } from 'vs/workbench/contrib/positronNotebook import { ServicesProvider } from 'vs/workbench/contrib/positronNotebook/browser/ServicesProvider'; import { GroupsOrder, + IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { PositronNotebookEditorInput } from './PositronNotebookEditorInput'; @@ -102,6 +103,7 @@ export class PositronNotebookEditor extends EditorPane { private _scopedInstantiationService?: IInstantiationService; constructor( + readonly _group: IEditorGroup, @IClipboardService readonly _clipboardService: IClipboardService, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -123,6 +125,7 @@ export class PositronNotebookEditor extends EditorPane { // Call the base class's constructor. super( PositronNotebookEditorInput.EditorID, + _group, telemetryService, themeService, storageService diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget.tsx index 5935084f6a5..6251821719e 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget.tsx @@ -4,8 +4,8 @@ import * as React from 'react'; import * as DOM from 'vs/base/browser/dom'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { FloatingEditorClickMenu } from 'vs/workbench/browser/codeeditor'; diff --git a/src/vs/workbench/contrib/positronPlots/browser/webviewPlotClient.ts b/src/vs/workbench/contrib/positronPlots/browser/webviewPlotClient.ts index 907731e817a..042f64ed719 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/webviewPlotClient.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/webviewPlotClient.ts @@ -1,7 +1,8 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ +import * as DOM from 'vs/base/browser/dom'; import { VSBuffer, encodeBase64 } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -25,6 +26,8 @@ export class WebviewPlotClient extends Disposable implements IPositronPlotClient private _renderTimer: NodeJS.Timeout | undefined; + private _element: HTMLElement | undefined; + /** * Creates a new WebviewPlotClient, which wraps a notebook output webview in * an object that can be displayed in the Plots pane. @@ -79,7 +82,7 @@ export class WebviewPlotClient extends Disposable implements IPositronPlotClient * @param claimant The object taking ownership. */ public claim(claimant: any) { - this.webview.webview.claim(claimant, undefined); + this.webview.webview.claim(claimant, DOM.getWindow(this._element), undefined); this._claimed = true; } @@ -89,6 +92,7 @@ export class WebviewPlotClient extends Disposable implements IPositronPlotClient * @param ele The element over which to position the webview. */ public layoutWebviewOverElement(ele: HTMLElement) { + this._element = ele; this.webview.webview.layoutWebviewOverElement(ele); } diff --git a/src/vs/workbench/contrib/positronPreview/browser/components/previewContainer.tsx b/src/vs/workbench/contrib/positronPreview/browser/components/previewContainer.tsx index 5608fdb1ae4..727a7d744d3 100644 --- a/src/vs/workbench/contrib/positronPreview/browser/components/previewContainer.tsx +++ b/src/vs/workbench/contrib/positronPreview/browser/components/previewContainer.tsx @@ -6,6 +6,7 @@ import 'vs/css!./previewContainer'; import * as React from 'react'; import { useEffect } from 'react'; // eslint-disable-line no-duplicate-imports import { PreviewWebview } from 'vs/workbench/contrib/positronPreview/browser/previewWebview'; +import * as DOM from 'vs/base/browser/dom'; /** * PreviewContainerProps interface. @@ -53,13 +54,14 @@ export const PreviewContainer = (props: PreviewContainerProps) => { // If the preview is visible, claim the webview and release it when // we're unmounted. if (props.visible) { - webview.claim(this, undefined); if (webviewRef.current) { + const window = DOM.getWindow(webviewRef.current); + webview.claim(this, window, undefined); webview.layoutWebviewOverElement(webviewRef.current); + return () => { + webview?.release(this); + }; } - return () => { - webview?.release(this); - }; } else { // If the preview is not visible, release the webview. webview.release(this); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 70920e27b23..2f92489d4b8 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/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/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index b0026da674c..480d9b5ab89 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -57,6 +57,10 @@ import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/common import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; 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.$; @@ -105,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, @@ -118,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()))); @@ -397,9 +402,9 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP const actions = [this.recordKeysAction, this.sortByPrecedenceAction, clearInputAction]; const toolBar = this._register(new ToolBar(this.actionsContainer, this.contextMenuService, { - actionViewItemProvider: (action: IAction) => { + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action.id === this.sortByPrecedenceAction.id || action.id === this.recordKeysAction.id) { - return new ToggleActionViewItem(null, action, { keybinding: this.keybindingsService.lookupKeybinding(action.id)?.getLabel(), toggleStyles: defaultToggleStyles }); + return new ToggleActionViewItem(null, action, { ...options, keybinding: this.keybindingsService.lookupKeybinding(action.id)?.getLabel(), toggleStyles: defaultToggleStyles }); } return undefined; }, @@ -896,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 { @@ -1034,14 +1051,14 @@ class SourceColumnRenderer implements ITableRenderer { this.extensionsWorkbenchService.open(extension.identifier.value); @@ -1057,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/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index f843df64aa3..adfccc8135c 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/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/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 19a69d1f1a0..078adf7dfa9 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/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/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 1be32db1a39..676d63374bb 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; -import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { BaseActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { HistoryInputBox, IHistoryInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { Action, IAction } from 'vs/base/common/actions'; @@ -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'); } @@ -252,7 +256,7 @@ export class SettingsTargetsWidget extends Widget { orientation: ActionsOrientation.HORIZONTAL, focusOnlyEnabledItems: true, ariaLabel: localize('settingsSwitcherBarAriaLabel', "Settings Switcher"), - actionViewItemProvider: (action: IAction) => action.id === 'folderSettings' ? this.folderSettings : undefined + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => action.id === 'folderSettings' ? this.folderSettings : undefined })); this.userLocalSettings = new Action('userSettings', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index c0632e494bc..3a5a03a9718 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -66,6 +66,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { CodeWindow } from 'vs/base/browser/window'; export const enum SettingsFocusContext { @@ -219,6 +220,7 @@ export class SettingsEditor2 extends EditorPane { private installedExtensionIds: string[] = []; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -240,7 +242,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, ) { - super(SettingsEditor2.ID, telemetryService, themeService, storageService); + super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); this.localSearchDelayer = new Delayer(300); this.remoteSearchThrottle = new ThrottledDelayer(200); @@ -398,7 +400,7 @@ export class SettingsEditor2 extends EditorPane { } private restoreCachedState(): ISettingsEditor2State | null { - const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input); + const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input); if (cachedState && typeof cachedState.target === 'object') { cachedState.target = URI.revive(cachedState.target); } @@ -499,8 +501,8 @@ export class SettingsEditor2 extends EditorPane { } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (!visible) { // Wait for editor to be removed from DOM #106303 @@ -645,7 +647,7 @@ export class SettingsEditor2 extends EditorPane { })); if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) { - const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer)); + const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; this.updateInputAriaLabel(); @@ -655,9 +657,9 @@ export class SettingsEditor2 extends EditorPane { this.controlsElement = DOM.append(searchContainer, DOM.$('.settings-clear-widget')); const actionBar = this._register(new ActionBar(this.controlsElement, { - actionViewItemProvider: (action) => { + actionViewItemProvider: (action, options) => { if (action.id === filterAction.id) { - return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, this.actionRunner, this.searchWidget); + return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, options, this.actionRunner, this.searchWidget); } return undefined; } @@ -1426,7 +1428,7 @@ export class SettingsEditor2 extends EditorPane { // If the context view is focused, delay rendering settings if (this.contextViewFocused()) { - const element = DOM.getWindow(this.settingsTree.getHTMLElement()).document.querySelector('.context-view'); + const element = this.window.document.querySelector('.context-view'); if (element) { this.scheduleRefresh(element as HTMLElement, key); } @@ -1830,10 +1832,10 @@ export class SettingsEditor2 extends EditorPane { if (this.isVisible()) { const searchQuery = this.searchWidget.getValue().trim(); const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget; - if (this.group && this.input) { + if (this.input) { this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target }); } - } else if (this.group && this.input) { + } else if (this.input) { this.editorMemento.clearEditorState(this.input, this.group); } @@ -1849,6 +1851,7 @@ class SyncControls extends Disposable { public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event; constructor( + window: CodeWindow, container: HTMLElement, @ICommandService private readonly commandService: ICommandService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -1881,7 +1884,7 @@ class SyncControls extends Disposable { })); const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer()); - updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, DOM.getWindow(container)); + updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window); this.update(); this._register(this.userDataSyncService.onDidChangeStatus(() => { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 2f32b9c5932..fd8f621714f 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/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/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts index 93e13c0234d..d119cd97f69 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { IAction, IActionRunner } from 'vs/base/common/actions'; @@ -17,6 +18,7 @@ export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenu constructor( action: IAction, + options: IActionViewItemOptions, actionRunner: IActionRunner | undefined, private readonly searchWidget: SuggestEnabledInput, @IContextMenuService contextMenuService: IContextMenuService @@ -25,6 +27,7 @@ export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenu { getActions: () => this.getActions() }, contextMenuService, { + ...options, actionRunner, classNames: action.class, anchorAlignmentProvider: () => AnchorAlignment.RIGHT, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 8a24222eac3..9b7e714c4ce 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/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/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 6675881177f..3cf230fa6ee 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/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 { + this.storageService.store(`${BANNER_REMOTE_UNSUPPORTED_CONNECTION_DISMISSED_KEY}`, this.productService.version, StorageScope.PROFILE, StorageTarget.MACHINE); + } + }); + } } else { this.hostService.openWindow({ forceReuseWindow: true, remoteAuthority: null }); return; diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 37948b2bf60..e9b9ce16497 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -217,8 +217,25 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon } } + private getPortAutoFallbackNumber(): number { + const fallbackAt = this.configurationService.inspect(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/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index f362b61aa1b..08dd88b0fae 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -49,6 +49,10 @@ import { infoIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcon import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { mainWindow } from 'vs/base/browser/window'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; type ActionGroup = [string, Array]; @@ -97,7 +101,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; @@ -121,27 +150,10 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { 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 +174,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 +188,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 +203,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 +217,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 +235,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } }); }; - }); + })); } } @@ -707,7 +719,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } } - if (this.extensionGalleryService.isEnabled() && this.remoteMetadataInitialized) { + const showExtensionRecommendations = this.configurationService.getValue('workbench.remoteIndicator.showExtensionRecommendations'); + if (showExtensionRecommendations && this.extensionGalleryService.isEnabled() && this.remoteMetadataInitialized) { const notInstalledItems: QuickPickItem[] = []; for (const metadata of this.remoteExtensionMetadata) { @@ -821,3 +834,15 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr quickPick.show(); } } + +Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration({ + ...workbenchConfigurationNodeBase, + properties: { + 'workbench.remoteIndicator.showExtensionRecommendations': { + type: 'boolean', + markdownDescription: nls.localize('remote.showExtensionRecommendations', "When enabled, remote extensions recommendations will be shown in the Remote Indicator menu."), + default: true + }, + } + }); diff --git a/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts b/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts index e4e2f80af4b..551a983f173 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts +++ b/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/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 102de6b52f8..49a702fbbfe 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/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); @@ -355,7 +355,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer(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/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 305f9bae1c5..0f750170457 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -395,7 +395,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private createExistingSessionItem(session: AuthenticationSession, providerId: string): ExistingSessionItem { return { label: session.account.label, - description: this.authenticationService.getLabel(providerId), + description: this.authenticationService.getProvider(providerId).label, session, providerId }; @@ -412,9 +412,9 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); - options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", providerName), provider: authenticationProvider }); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!signedInForProvider || provider.supportsMultipleAccounts) { + options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", provider.label), provider: authenticationProvider }); } } @@ -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/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 742c76b7d37..7537f80f534 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -29,7 +29,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { rot } from 'vs/base/common/numbers'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { IDiffEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Action, IAction, ActionRunner } from 'vs/base/common/actions'; import { IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -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/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index f91410f9b8d..544b8d274a7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -43,7 +43,7 @@ import { flatten } from 'vs/base/common/arrays'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -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/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.contribution.ts b/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.contribution.ts new file mode 100644 index 00000000000..ad8bc56733b --- /dev/null +++ b/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/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.ts b/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.ts new file mode 100644 index 00000000000..8e3facaa7e9 --- /dev/null +++ b/src/vs/workbench/contrib/scrollLocking/browser/scrollLocking.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { 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 { 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 { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; + +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 statusBarEntry = this._register(new MutableDisposable()); + + private isActive: boolean = false; + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IStatusbarService private readonly statusbarService: IStatusbarService + ) { + 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); + } + + // makes sure that the onDidEditorPaneScroll is not called multiple times for the same event + private _reentrancyBarrier = new ReentrancyBarrier(); + + 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._reentrancyBarrier.runExclusively(() => { + 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)) { + continue; + } + + const initialOffset = this.paneInitialScrollTop.get(pane); + if (initialOffset === undefined) { + throw new Error('Could not find initial offset for pane'); + } + + const currentPanePosition = pane.getScrollPosition(); + const newPaneScrollPosition = { + scrollTop: initialOffset.scrollTop + scrolledFromInitial.scrollTop, + scrollLeft: initialOffset.scrollLeft !== undefined && scrolledFromInitial.scrollLeft !== undefined ? initialOffset.scrollLeft + scrolledFromInitial.scrollLeft : undefined, + }; + + if (currentPanePosition.scrollTop === newPaneScrollPosition.scrollTop && currentPanePosition.scrollLeft === newPaneScrollPosition.scrollLeft) { + continue; + } + + pane.setScrollPosition(newPaneScrollPosition); + } + } + + 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 toggleStatusbarItem(active: boolean): void { + if (active) { + if (!this.statusBarEntry.value) { + const text = localize('mouseScrolllingLocked', 'Scrolling Locked'); + const tooltip = localize('mouseLockScrollingEnabled', 'Lock Scrolling Enabled'); + this.statusBarEntry.value = this.statusbarService.addEntry({ + name: text, + text, + tooltip, + ariaLabel: text, + command: { + id: 'workbench.action.toggleLockedScrolling', + title: '' + }, + kind: 'prominent', + showInAllWindows: true + }, 'status.scrollLockingEnabled', StatusbarAlignment.RIGHT, 102); + } + } else { + this.statusBarEntry.clear(); + } + } + + private registerActions() { + const $this = this; + 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.deactivate(); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index a85e27af833..489a46c5544 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/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'; @@ -55,6 +55,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { ASK_QUICK_QUESTION_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ILogService } from 'vs/platform/log/common/log'; interface IAnythingQuickPickItem extends IPickerQuickAccessItem, IQuickPickItemWithResource { } @@ -83,11 +84,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 +101,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider): void { @@ -129,7 +133,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)); } @@ -628,6 +646,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + const start = Date.now(); return this.searchService.fileSearch( this.fileQueryBuilder.file( this.contextService.getWorkspace().folders, @@ -636,7 +655,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + this.logService.trace(`QuickAccess fileSearch ${Date.now() - start}ms`); + }); } private getFileQueryOptions(input: { filePattern?: string; cacheKey?: string; maxResults?: number }): IFileQueryBuilderOptions { @@ -844,7 +865,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { @@ -271,6 +273,7 @@ export class ExcludePatternInputWidget extends PatternInputWidget { actionClassName: 'useExcludesAndIgnoreFiles', title: nls.localize('useExcludesAndIgnoreFilesDescription', "Use Exclude Settings and Ignore Files"), isChecked: true, + hoverDelegate: getDefaultHoverDelegate('element'), ...defaultToggleStyles })); this._register(this.useExcludesAndIgnoreFilesBox.onChange(viaKeyboard => { diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index 3691e98ec1d..3172a47ff34 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -15,9 +15,9 @@ import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; -import { FastAndSlowPicks, IPickerQuickAccessItem, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { FastAndSlowPicks, IPickerQuickAccessItem, IPickerQuickAccessSeparator, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; -import { IKeyMods, IQuickPick, IQuickPickItem, IQuickPickSeparator, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { searchDetailsIcon, searchOpenInFileIcon, searchActivityBarIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -28,9 +28,8 @@ import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/ import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { IPatternInfo, ISearchComplete, ITextQuery, VIEW_ID } from 'vs/workbench/services/search/common/search'; import { Event } from 'vs/base/common/event'; -import { EditorViewState } from 'vs/workbench/browser/quickaccess'; +import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { Sequencer } from 'vs/base/common/async'; export const TEXT_SEARCH_QUICK_ACCESS_PREFIX = '%'; @@ -45,6 +44,7 @@ const DEFAULT_TEXT_QUERY_BUILDER_OPTIONS: ITextQueryBuilderOptions = { const MAX_FILES_SHOWN = 30; const MAX_RESULTS_PER_FILE = 10; +const DEBOUNCE_DELAY = 75; interface ITextSearchQuickAccessItem extends IPickerQuickAccessItem { match?: Match; @@ -58,9 +58,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider { + + const onDidChangeActive = () => { const [item] = picker.activeItems; if (item?.match) { @@ -124,28 +123,26 @@ export class TextSearchQuickAccess 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.debounce(picker.onDidChangeActive, (last, event) => event, DEBOUNCE_DELAY, true)(onDidChangeActive)); + 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 +222,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) { @@ -257,11 +254,15 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider => { + await this.handleAccept(fileMatch, {}); + return TriggerAction.CLOSE_PICKER; + }, }); const results: Match[] = fileMatch.matches() ?? []; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 0531e3a3776..4a31def5cbb 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -128,7 +128,7 @@ configurationRegistry.registerConfiguration({ properties: { [SEARCH_EXCLUDE_CONFIG]: { type: 'object', - markdownDescription: nls.localize('exclude', "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for excluding files and folders in fulltext searches and quick open. Inherits all glob patterns from the `#files.exclude#` setting."), + markdownDescription: nls.localize('exclude', "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for excluding files and folders in fulltext searches and file search in quick open. To exclude files from the recently opened list in quick open, patterns must be absolute (for example `**/node_modules/**`). Inherits all glob patterns from the `#files.exclude#` setting."), default: { '**/node_modules': true, '**/bower_components': true, '**/*.code-search': true }, additionalProperties: { anyOf: [ @@ -308,6 +308,16 @@ configurationRegistry.registerConfiguration({ ], markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double-clicking a result in a search editor.") }, + 'search.searchEditor.singleClickBehaviour': { + type: 'string', + enum: ['default', 'peekDefinition',], + default: 'default', + enumDescriptions: [ + nls.localize('search.searchEditor.singleClickBehaviour.default', "Single-clicking does nothing."), + nls.localize('search.searchEditor.singleClickBehaviour.peekDefinition', "Single-clicking opens a Peek Definition window."), + ], + markdownDescription: nls.localize('search.searchEditor.singleClickBehaviour', "Configure effect of single-clicking a result in a search editor.") + }, 'search.searchEditor.reusePriorSearchConfiguration': { type: 'boolean', default: false, diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index e7dbb128b7b..a253cc2738b 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/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/src/vs/workbench/contrib/search/browser/searchFindInput.ts b/src/vs/workbench/contrib/search/browser/searchFindInput.ts index 0d8db8e91d3..6276fc46539 100644 --- a/src/vs/workbench/contrib/search/browser/searchFindInput.ts +++ b/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._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.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/src/vs/workbench/contrib/search/browser/searchIcons.ts b/src/vs/workbench/contrib/search/browser/searchIcons.ts index f81dc87d394..066fbb8c836 100644 --- a/src/vs/workbench/contrib/search/browser/searchIcons.ts +++ b/src/vs/workbench/contrib/search/browser/searchIcons.ts @@ -29,3 +29,6 @@ export const searchViewIcon = registerIcon('search-view-icon', Codicon.search, l export const searchNewEditorIcon = registerIcon('search-new-editor', Codicon.newFile, localize('searchNewEditorIcon', 'Icon for the action to open a new search editor.')); export const searchOpenInFileIcon = registerIcon('search-open-in-file', Codicon.goToFile, localize('searchOpenInFile', 'Icon for the action to go to the file of the current search result.')); + +export const searchSparkleFilled = registerIcon('search-sparkle-filled', Codicon.sparkleFilled, localize('searchSparkleFilled', 'Icon to show AI results in search.')); +export const searchSparkleEmpty = registerIcon('search-sparkle-empty', Codicon.sparkle, localize('searchSparkleEmpty', 'Icon to hide AI results in search.')); diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 0f280b7f1d0..4430f0590ea 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/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,7 +2011,9 @@ 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; + private aiSearchCancelledForNewSearch: boolean = false; public location: SearchModelLocation = SearchModelLocation.PANEL; constructor( @@ -1971,6 +2023,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 +2066,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 +2124,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 +2225,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 +2245,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 +2295,35 @@ 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 + (ai ? this.aiSearchCancelledForNewSearch : this.searchCancelledForNewSearch) ? { exit: SearchCompletionExitCode.NewSearchStarted, results: [], messages: [] } : undefined, - duration, ''); - this.searchCancelledForNewSearch = false; + duration, '', ai); + if (ai) { + this.aiSearchCancelledForNewSearch = false; + } else { + 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; } }); } @@ -2227,9 +2343,17 @@ export class SearchModel extends Disposable { } return false; } - + cancelAISearch(cancelledForNewSearch = false): boolean { + if (this.currentAICancelTokenSource) { + this.aiSearchCancelledForNewSearch = cancelledForNewSearch; + this.currentAICancelTokenSource.cancel(); + return true; + } + return false; + } override dispose(): void { this.cancelSearch(); + this.cancelAISearch(); this.searchResult.dispose(); super.dispose(); } @@ -2354,16 +2478,16 @@ export class RangeHighlightDecorations implements IDisposable { -function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch): Match[] { +function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch, isAiContributed: boolean): Match[] { const previewLines = rawMatch.preview.text.split('\n'); if (Array.isArray(rawMatch.ranges)) { return rawMatch.ranges.map((r, i) => { const previewRange: ISearchRange = (rawMatch.preview.matches)[i]; - return new Match(fileMatch, previewLines, previewRange, r); + return new Match(fileMatch, previewLines, previewRange, r, isAiContributed); }); } else { const previewRange = rawMatch.preview.matches; - const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges); + const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges, isAiContributed); return [match]; } } diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 3f71827f12c..e22115c2a8f 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -30,6 +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/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; interface IFolderMatchTemplate { label: IResourceLabel; @@ -360,7 +362,9 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.match.classList.toggle('replace', replace); templateData.replace.textContent = replace ? match.replaceString : ''; templateData.after.textContent = preview.after; - templateData.parent.title = (preview.fullBefore + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999); + + const title = (preview.fullBefore + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999); + templateData.disposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), templateData.parent, title)); SearchContext.IsEditableItemKey.bindTo(templateData.contextKeyService).set(!(match instanceof MatchInNotebook && match.isReadonly())); @@ -372,7 +376,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.lineNumber.classList.toggle('show', (numLines > 0) || showLineNumbers); templateData.lineNumber.textContent = lineNumberStr + extraLinesStr; - templateData.lineNumber.setAttribute('title', this.getMatchTitle(match, showLineNumbers)); + templateData.disposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), templateData.lineNumber, this.getMatchTitle(match, showLineNumbers))); templateData.actions.context = { viewer: this.searchView.getControl(), element: match }; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 649a025c5c3..379fd061360 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -23,7 +23,7 @@ import * as network from 'vs/base/common/network'; import 'vs/css!./media/searchview'; import { getCodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Selection } from 'vs/editor/common/core/selection'; import { IEditor } from 'vs/editor/common/editorCommon'; @@ -75,12 +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/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const $ = dom.$; @@ -156,6 +158,7 @@ export class SearchView extends ViewPane { private treeAccessibilityProvider: SearchAccessibilityProvider; private treeViewKey: IContextKey; + private aiResultsVisibleKey: IContextKey; private _visibleMatches: number = 0; @@ -216,6 +219,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._register(this.contextKeyService.onDidChangeContext(e => { + const keys = Constants.SearchContext.hasAIResultProvider.keys(); + if (e.affectsSome(new Set(keys))) { + this.refreshHasAISetting(); + } + })); // scoped this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container)); @@ -228,7 +239,7 @@ export class SearchView extends ViewPane { this.instantiationService = this.instantiationService.createChild( new ServiceCollection([IContextKeyService, this.contextKeyService])); - this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('search.sortOrder')) { if (this.searchConfig.sortOrder === SearchSortOrder.Modified) { // If changing away from modified, remove all fileStats @@ -236,8 +247,10 @@ export class SearchView extends ViewPane { this.removeFileStats(); } this.refreshTree(); + } else if (e.affectsConfiguration('search.aiResults')) { + this.refreshHasAISetting(); } - }); + })); this.viewModel = this._register(this.searchViewModelWorkbenchService.searchModel); this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); @@ -292,6 +305,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; @@ -301,6 +322,26 @@ 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; + } + + // in each case, we want to cancel our current AI search because it is no longer valid + this.model.cancelAISearch(); + 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; } @@ -321,6 +362,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); @@ -374,8 +421,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); } @@ -388,6 +435,7 @@ export class SearchView extends ViewPane { this.searchWidgetsContainerElement = dom.append(this.container, $('.search-widgets-container')); this.createSearchWidget(this.searchWidgetsContainerElement); + this.refreshHasAISetting(); const history = this.searchHistoryService.load(); const filePatterns = this.viewletState['query.filePatterns'] || ''; @@ -405,7 +453,8 @@ export class SearchView extends ViewPane { // Toggle query details button this.toggleQueryDetailsButton = dom.append(this.queryDetails, - $('.more' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button', title: nls.localize('moreSearch', "Toggle Search Details") })); + $('.more' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); + this._register(setupCustomHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, nls.localize('moreSearch', "Toggle Search Details"))); this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => { dom.EventHelper.stop(e); @@ -569,7 +618,8 @@ export class SearchView extends ViewPane { isInNotebookMarkdownPreview, isInNotebookCellInput, isInNotebookCellOutput, - } + }, + initialAIButtonVisibility: this.shouldShowAIButton() })); if (!this.searchWidget.searchInput || !this.searchWidget.replaceInput) { @@ -583,7 +633,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); @@ -622,7 +679,10 @@ export class SearchView extends ViewPane { this.trackInputBox(this.searchWidget.replaceInputFocusTracker); } - + private shouldShowAIButton(): boolean { + const hasProvider = Constants.SearchContext.hasAIResultProvider.getValue(this.contextKeyService); + return !!(this.configurationService.getValue('search.aiResults') && hasProvider); + } private onConfigurationUpdated(event?: IConfigurationChangeEvent): void { if (event && (event.affectsConfiguration('search.decorations.colors') || event.affectsConfiguration('search.decorations.badges'))) { this.refreshTree(); @@ -657,7 +717,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); } @@ -689,7 +749,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); @@ -724,7 +784,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 })); } @@ -1262,7 +1326,7 @@ export class SearchView extends ViewPane { } hasSearchResults(): boolean { - return !this.viewModel.searchResult.isEmpty(); + return !this.viewModel.searchResult.isEmpty(this.aiResultsVisible); } clearSearchResults(clearInput = true): void { @@ -1573,6 +1637,7 @@ export class SearchView extends ViewPane { }); this.viewModel.cancelSearch(true); + this.viewModel.cancelAISearch(true); this.currentSearchQ = this.currentSearchQ .then(() => this.doSearch(query, excludePatternText, includePatternText, triggeredOnType)) @@ -1586,7 +1651,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(); @@ -1608,14 +1673,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; } @@ -1683,7 +1748,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())); } @@ -1738,6 +1803,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); @@ -1782,13 +1861,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); @@ -1984,7 +2064,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]); @@ -2105,7 +2191,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); } @@ -2118,6 +2204,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 { @@ -2133,7 +2222,8 @@ class SearchLinkButton extends Disposable { constructor(label: string, handler: (e: dom.EventLike) => unknown, tooltip?: string) { super(); - this.element = $('a.pointer', { tabindex: 0, title: tooltip }, label); + this.element = $('a.pointer', { tabindex: 0 }, label); + this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.element, tooltip)); this.addEventHandlers(handler); } diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 26acd42d459..5bf1f66744f 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/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,6 +42,8 @@ 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/hoverDelegateFactory'; +import { IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; /** Specified in searchview.css */ const SingleLineInputHeight = 26; @@ -60,6 +62,7 @@ export interface ISearchWidgetOptions { inputBoxStyles: IInputBoxStyles; toggleStyles: IToggleStyles; notebookOptions?: NotebookToggleState; + initialAIButtonVisibility?: boolean; } interface NotebookToggleState { @@ -119,7 +122,7 @@ export class SearchWidget extends Widget { domNode: HTMLElement | undefined; - searchInput: FindInput | undefined; + searchInput: SearchFindInput | undefined; searchInputFocusTracker: dom.IFocusTracker | undefined; private searchInputBoxFocused: IContextKey; @@ -169,6 +172,7 @@ export class SearchWidget extends Widget { public contextLinesInput!: InputBox; private _notebookFilters: NotebookFindFilters; + private _toggleReplaceButtonListener: MutableDisposable; constructor( container: HTMLElement, @@ -205,12 +209,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(); @@ -218,6 +222,7 @@ export class SearchWidget extends Widget { })); this._replaceHistoryDelayer = new Delayer(500); + this._toggleReplaceButtonListener = this._register(new MutableDisposable()); this.render(container, options); @@ -370,15 +375,15 @@ export class SearchWidget extends Widget { buttonSecondaryBackground: undefined, buttonSecondaryForeground: undefined, buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined + buttonSeparator: undefined, + title: nls.localize('search.replace.toggle.button.title', "Toggle Replace"), + hoverDelegate: getDefaultHoverDelegate('element'), }; this.toggleReplaceButton = this._register(new Button(parent, opts)); this.toggleReplaceButton.element.setAttribute('aria-expanded', 'false'); this.toggleReplaceButton.element.classList.add('toggle-replace-button'); this.toggleReplaceButton.icon = searchHideReplaceIcon; - // TODO@joao need to dispose this listener eventually - this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton()); - this.toggleReplaceButton.element.title = nls.localize('search.replace.toggle.button.title', "Toggle Replace"); + this._toggleReplaceButtonListener.value = this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton()); } private renderSearchInput(parent: HTMLElement, options: ISearchWidgetOptions): void { @@ -400,7 +405,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 || ''); @@ -441,6 +458,7 @@ export class SearchWidget extends Widget { isChecked: false, title: appendKeyBindingLabel(nls.localize('showContext', "Toggle Context Lines"), this.keybindingService.lookupKeybinding(ToggleSearchEditorContextLinesCommandId)), icon: searchShowContextIcon, + hoverDelegate: getDefaultHoverDelegate('element'), ...defaultToggleStyles }); this._register(this.showContextToggle.onChange(() => this.onContextLinesChanged())); @@ -582,6 +600,7 @@ export class SearchWidget extends Widget { this.setReplaceAllActionState(false); if (this.searchConfiguration.searchOnType) { + const delayMultiplierFromAISearch = (this.searchInput && this.searchInput.isAIEnabled) ? 5 : 1; // expand debounce period to multiple by 5 if AI is enabled if (this.searchInput?.getRegex()) { try { const regex = new RegExp(this.searchInput.getValue(), 'ug'); @@ -600,12 +619,13 @@ export class SearchWidget extends Widget { matchienessHeuristic < 100 ? 5 : // expressions like `.` or `\w` 10; // only things matching empty string - this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier); + + this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier * delayMultiplierFromAISearch); } catch { // pass } } else { - this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod); + this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplierFromAISearch); } } } diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index a8f1bc84311..a7f7c53b660 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -41,6 +41,8 @@ export const enum SearchCommandIds { ClearSearchResultsActionId = 'search.action.clearSearchResults', ViewAsTreeActionId = 'search.action.viewAsTree', ViewAsListActionId = 'search.action.viewAsList', + ShowAIResultsActionId = 'search.action.showAIResults', + HideAIResultsActionId = 'search.action.hideAIResults', ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails', ExcludeFolderFromSearchId = 'search.action.excludeFromSearch', FocusNextInputActionId = 'search.focus.nextInputBox', @@ -74,4 +76,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/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 5e39773a0cc..9b3b9e4f4dd 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -125,6 +125,7 @@ suite('Search Actions', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } @@ -145,7 +146,8 @@ suite('Search Actions', () => { startColumn: 0, endLineNumber: line, endColumn: 2 - } + }, + false ); fileMatch.add(match); return match; diff --git a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts index 24e2448a9b3..17f9e88b338 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts +++ b/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/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index 0a591e21457..a90875c1384 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -217,6 +217,7 @@ suite('searchNotebookHelpers', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(folderMatch); store.add(fileMatch); diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index c6d94a0ebf3..e71fab4b131 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -66,7 +66,7 @@ suite('SearchResult', () => { test('Line Match', function () { const fileMatch = aFileMatch('folder/file.txt', null!); - const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5)); + const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5), false); assert.strictEqual(lineMatch.text(), '0 foo bar'); assert.strictEqual(lineMatch.range().startLineNumber, 2); assert.strictEqual(lineMatch.range().endLineNumber, 2); @@ -174,7 +174,7 @@ suite('SearchResult', () => { const searchResult = instantiationService.createInstance(SearchResult, searchModel); store.add(searchResult); const fileMatch = aFileMatch('far/boo', searchResult); - const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3)); + const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3), false); assert(lineMatch.parent() === fileMatch); assert(fileMatch.parent() === searchResult.folderMatches()[0]); @@ -532,6 +532,7 @@ suite('SearchResult', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, root, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; diff --git a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts index 5c5fcd10aab..b6e7dc04bbb 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts @@ -66,5 +66,5 @@ export function stubNotebookEditorService(instantiationService: TestInstantiatio } export function addToSearchResult(searchResult: SearchResult, allRaw: IFileMatch[], searchInstanceID = '') { - searchResult.add(allRaw, searchInstanceID); + searchResult.add(allRaw, searchInstanceID, false); } diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 76b49f5f2bf..4feb3d343ed 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -76,7 +76,7 @@ suite('Search - Viewlet', () => { endColumn: 1 } }] - }], ''); + }], '', false); const fileMatch = result.matches()[0]; const lineMatch = fileMatch.matches()[0]; @@ -89,9 +89,9 @@ suite('Search - Viewlet', () => { const fileMatch1 = aFileMatch('/foo'); const fileMatch2 = aFileMatch('/with/path'); const fileMatch3 = aFileMatch('/with/path/foo'); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); - const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); + const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); assert(searchMatchComparer(fileMatch2, fileMatch1) > 0); @@ -127,13 +127,13 @@ suite('Search - Viewlet', () => { const fileMatch2 = aFileMatch('/with/path.c', folderMatch2); const fileMatch3 = aFileMatch('/with/path/bar.b', folderMatch2); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); /*** * Structure would take the following form: @@ -180,6 +180,7 @@ suite('Search - Viewlet', () => { const fileMatch = instantiation.createInstance(FileMatch, { pattern: '' }, undefined, undefined, parentFolder ?? aFolderMatch('', 0), rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 5f36473df15..84bb2199629 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -13,7 +13,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; -import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -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,6 +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/hover/updatableHoverWidget'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; const RESULT_LINE_REGEX = /^(\s+)(\d+)(: | )(\s*)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; @@ -95,6 +97,7 @@ export class SearchEditor extends AbstractTextCodeEditor private updatingModelForSearch: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -114,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)); @@ -161,7 +164,8 @@ export class SearchEditor extends AbstractTextCodeEditor this.includesExcludesContainer = DOM.append(container, DOM.$('.includes-excludes')); // Toggle query details button - this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button', title: localize('moreSearch', "Toggle Search Details") })); + this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); + this._register(setupCustomHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, localize('moreSearch', "Toggle Search Details"))); this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.CLICK, e => { DOM.EventHelper.stop(e); this.toggleIncludesExcludes(); @@ -245,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') { @@ -655,7 +669,7 @@ export class SearchEditor extends AbstractTextCodeEditor } private getInput(): SearchEditorInput | undefined { - return this._input as SearchEditorInput; + return this.input as SearchEditorInput; } private priorConfig: Partial> | undefined; diff --git a/src/vs/workbench/contrib/share/browser/share.contribution.ts b/src/vs/workbench/contrib/share/browser/share.contribution.ts index ae071141610..5bdf93d7236 100644 --- a/src/vs/workbench/contrib/share/browser/share.contribution.ts +++ b/src/vs/workbench/contrib/share/browser/share.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./share'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -110,7 +110,7 @@ class ShareWorkbenchContribution { const result = await progressService.withProgress({ location: ProgressLocation.Window, detail: localize('generating link', 'Generating link...') - }, async () => shareService.provideShare({ resourceUri, selection }, new CancellationTokenSource().token)); + }, async () => shareService.provideShare({ resourceUri, selection }, CancellationToken.None)); if (result) { const uriText = result.toString(); diff --git a/src/vs/workbench/contrib/speech/browser/speech.contribution.ts b/src/vs/workbench/contrib/speech/browser/speech.contribution.ts index 03a6035fb80..7184018cd98 100644 --- a/src/vs/workbench/contrib/speech/browser/speech.contribution.ts +++ b/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/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 00943f3262b..4d13c101203 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/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,19 +139,15 @@ 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(); let sessionRecognized = false; + let sessionContentLength = 0; const disposables = new DisposableStore(); @@ -92,24 +155,31 @@ 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.' }; + sessionContentLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Length of the recognized text.' }; + sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; }; type SpeechToTextSessionEvent = { context: string; - duration: number; - recognized: boolean; + sessionDuration: number; + sessionRecognized: boolean; + sessionContentLength: number; + sessionLanguage: string; }; this.telemetryService.publicLog2('speechToTextSession', { context, - duration: Date.now() - sessionStart, - recognized: sessionRecognized + sessionDuration: Date.now() - sessionStart, + sessionRecognized, + sessionContentLength, + sessionLanguage: language }); } @@ -127,12 +197,17 @@ 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: - case SpeechToTextStatus.Recognized: sessionRecognized = true; break; + case SpeechToTextStatus.Recognized: + if (typeof e.text === 'string') { + sessionContentLength += e.text.length; + } + break; case SpeechToTextStatus.Stopped: onSessionStoppedOrCanceled(); break; @@ -142,6 +217,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 +244,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 +294,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/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index a594b4f3acf..6aced99f16e 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/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/src/vs/workbench/contrib/speech/test/common/speechService.test.ts b/src/vs/workbench/contrib/speech/test/common/speechService.test.ts new file mode 100644 index 00000000000..d757eace7e0 --- /dev/null +++ b/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/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 43246667cfe..5605c99c5d8 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/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/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index d07edd2679c..9f47ca8564e 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/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/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index a2d81dfbe73..d25127a5524 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts index 7710088cf64..12b5058d65a 100644 --- a/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index e8c7ac8bada..406e5697c4e 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/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/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh index cc2cb83e0d2..d54b124e69a 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh +++ b/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/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index 53e9bd30602..69b5bea7875 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/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 @@ -63,29 +63,14 @@ function Global:Prompt() { # error when $LastHistoryEntry is null, and is not otherwise useful. Set-StrictMode -Off $LastHistoryEntry = Get-History -Count 1 + $Result = "" # Skip finishing the command if the first command has not yet started if ($Global:__LastHistoryId -ne -1) { if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) { # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command) - $Result = "$([char]0x1b)]633;E`a" $Result += "$([char]0x1b)]633;D`a" } else { - # Command finished command line - # OSC 633 ; E ; ; ST - $Result = "$([char]0x1b)]633;E;" - # Sanitize the command line to ensure it can get transferred to the terminal and can be parsed - # correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter - # to only be composed of _printable_ characters as per the spec. - if ($LastHistoryEntry.CommandLine) { - $CommandLine = $LastHistoryEntry.CommandLine - } - else { - $CommandLine = "" - } - $Result += $(__VSCode-Escape-Value $CommandLine) - $Result += ";$Nonce" - $Result += "`a" # Command finished exit code # OSC 633 ; D [; ] ST $Result += "$([char]0x1b)]633;D;$FakeCode`a" @@ -114,10 +99,24 @@ function Global:Prompt() { if (Get-Module -Name PSReadLine) { $__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine function Global:PSConsoleHostReadLine { - $tmp = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() + $CommandLine = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() + + # Command line + # OSC 633 ; E ; ; ST + $Result = "$([char]0x1b)]633;E;" + $Result += $(__VSCode-Escape-Value $CommandLine) + $Result += ";$Nonce" + $Result += "`a" + [Console]::Write($Result) + + # Command executed + # OSC 633 ; C ST + $Result += "$([char]0x1b)]633;C`a" + # Write command executed sequence directly to Console to avoid the new line from Write-Host - [Console]::Write("$([char]0x1b)]633;C`a") - $tmp + [Console]::Write($Result) + + $CommandLine } } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 488815cf258..e30f5dd92d9 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/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/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts index 524a251532a..7841d76d329 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index ff806eba0ec..8889ca0a961 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -4,56 +4,55 @@ *--------------------------------------------------------------------------------------------*/ 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 { TerminalLogService } from 'vs/platform/terminal/common/terminalLogService'; +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'; // Register services registerSingleton(ITerminalLogService, TerminalLogService, InstantiationType.Delayed); @@ -79,9 +78,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/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 9a1bd1ab1b7..70018beda53 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/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 { @@ -262,6 +267,7 @@ export interface ITerminalService extends ITerminalInstanceHost { readonly onDidChangeActiveGroup: Event; // Multiplexed events + readonly onAnyInstanceData: Event<{ instance: ITerminalInstance; data: string }>; readonly onAnyInstanceDataInput: Event; readonly onAnyInstanceIconChange: Event<{ instance: ITerminalInstance; userInitiated: boolean }>; readonly onAnyInstanceMaximumDimensionsChange: Event; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 50a7fc56a29..5c1b4ef531c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/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) => { @@ -1776,6 +1776,15 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { type: 'string', enum: profileEnum.values, markdownEnumDescriptions: profileEnum.markdownDescriptions + }, + location: { + description: localize('newWithProfile.location', "Where to create the terminal"), + type: 'string', + enum: ['view', 'editor'], + enumDescriptions: [ + localize('newWithProfile.location.view', 'Create the terminal in the terminal view'), + localize('newWithProfile.location.editor', 'Create the terminal in the editor'), + ] } } } @@ -1783,7 +1792,11 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { }, }); } - async run(accessor: ServicesAccessor, eventOrOptionsOrProfile: MouseEvent | ICreateTerminalOptions | ITerminalProfile | { profileName: string } | undefined, profile?: ITerminalProfile) { + async run( + accessor: ServicesAccessor, + eventOrOptionsOrProfile: MouseEvent | ICreateTerminalOptions | ITerminalProfile | { profileName: string; location?: 'view' | 'editor' | unknown } | undefined, + profile?: ITerminalProfile + ) { const c = getTerminalServices(accessor); const workspaceContextService = accessor.get(IWorkspaceContextService); const commandService = accessor.get(ICommandService); @@ -1799,6 +1812,12 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { throw new Error(`Could not find terminal profile "${eventOrOptionsOrProfile.profileName}"`); } options = { config }; + if ('location' in eventOrOptionsOrProfile) { + switch (eventOrOptionsOrProfile.location) { + case 'editor': options.location = TerminalLocation.Editor; break; + case 'view': options.location = TerminalLocation.Panel; break; + } + } } else if (isMouseEvent(eventOrOptionsOrProfile) || isPointerEvent(eventOrOptionsOrProfile) || isKeyboardEvent(eventOrOptionsOrProfile)) { event = eventOrOptionsOrProfile; options = profile ? { config: profile } : undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts new file mode 100644 index 00000000000..3f8dd46a31a --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +// eslint-disable-next-line local/code-import-patterns +export { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 6835b5e9c35..900fbaf9fed 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -47,6 +47,7 @@ export class TerminalEditor extends EditorPane { private _cancelContextMenu: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -61,7 +62,7 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService ) { - super(terminalEditorId, telemetryService, themeService, storageService); + super(terminalEditorId, group, telemetryService, themeService, storageService); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, contextKeyService)); this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalInstanceContext, contextKeyService)); } @@ -74,7 +75,7 @@ export class TerminalEditor extends EditorPane { if (this._lastDimension) { this.layout(this._lastDimension); } - this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); if (this._editorInput.terminalInstance) { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set @@ -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/src/vs/workbench/contrib/terminal/browser/terminalEvents.ts b/src/vs/workbench/contrib/terminal/browser/terminalEvents.ts index 9f1630864a8..dc1d7a2c515 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEvents.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index d993243770f..171c69d6ed3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index b177b4fad02..caa2bbd3fa5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 84bde0d8006..99f6f8b4357 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -721,7 +721,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, @@ -824,10 +824,29 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } async runCommand(commandLine: string, shouldExecute: boolean): Promise { + let commandDetection = this.capabilities.get(TerminalCapability.CommandDetection); + + // Await command detection if the terminal is starting up + if (!commandDetection && (this._processManager.processState === ProcessState.Uninitialized || this._processManager.processState === ProcessState.Launching)) { + const store = new DisposableStore(); + await Promise.race([ + new Promise(r => { + store.add(this.capabilities.onDidAddCapabilityType(e => { + if (e === TerminalCapability.CommandDetection) { + commandDetection = this.capabilities.get(TerminalCapability.CommandDetection); + r(); + } + })); + }), + timeout(2000), + ]); + store.dispose(); + } + // Determine whether to send ETX (ctrl+c) before running the command. This should always // happen unless command detection can reliably say that a command is being entered and // there is no content in the prompt - if (this.capabilities.get(TerminalCapability.CommandDetection)?.hasInput !== false) { + if (commandDetection?.hasInput !== false) { await this.sendText('\x03', false); // Wait a little before running the command to avoid the sequences being echoed while the ^C // is being evaluated @@ -1459,23 +1478,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/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts b/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts index 69fe49e69da..2ff5e51cfe6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 7f119914b09..a000c5e8465 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 81770243f91..b731bf456fd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -40,6 +40,7 @@ import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } import { generateUuid } from 'vs/base/common/uuid'; import { getActiveWindow, runWhenWindowIdle } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; const enum ProcessConstants { /** @@ -436,7 +437,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); - if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (!this._isDisposed && shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index b7afa342a67..116e678eacd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -6,7 +6,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQuickInputService, IKeyMods, IPickOptions, IQuickPickSeparator, IQuickInputButton, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IExtensionTerminalProfile, ITerminalProfile, ITerminalProfileObject, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; +import { IExtensionTerminalProfile, ITerminalProfile, ITerminalProfileObject, TerminalSettingPrefix, type ITerminalExecutable } from 'vs/platform/terminal/common/terminal'; import { getUriClasses, getColorClass, createColorStyleElement } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import * as nls from 'vs/nls'; @@ -131,11 +131,14 @@ export class TerminalProfileQuickpick { if (!name) { return; } - const newConfigValue: { [key: string]: ITerminalProfileObject } = { ...configProfiles }; - newConfigValue[name] = { - path: context.item.profile.path, - args: context.item.profile.args - }; + const newConfigValue: { [key: string]: ITerminalExecutable } = { ...configProfiles }; + newConfigValue[name] = { path: context.item.profile.path }; + if (context.item.profile.args) { + newConfigValue[name].args = context.item.profile.args; + } + if (context.item.profile.env) { + newConfigValue[name].env = context.item.profile.env; + } await this._configurationService.updateValue(profilesKey, newConfigValue, ConfigurationTarget.USER); }, onKeyMods: mods => keyMods = mods diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index 63dafffca62..87a2bb83a65 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts index e1ab421d423..c6f6bdb61d4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts @@ -218,7 +218,7 @@ export async function showRunRecentQuickPick( instantiationService.invokeFunction(showRunRecentQuickPick, instance, terminalInRunCommandPicker, type, fuzzySearchToggle.checked ? 'fuzzy' : 'contiguous', quickPick.value); }); const outputProvider = instantiationService.createInstance(TerminalOutputProvider); - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick(); const originalItems = items; quickPick.items = [...originalItems]; quickPick.sortByLabel = false; @@ -258,6 +258,39 @@ export async function showRunRecentQuickPick( await instantiationService.invokeFunction(showRunRecentQuickPick, instance, terminalInRunCommandPicker, type, filterMode, value); } }); + let terminalScrollStateSaved = false; + function restoreScrollState() { + terminalScrollStateSaved = false; + instance.xterm?.markTracker.restoreScrollState(); + instance.xterm?.markTracker.clear(); + } + quickPick.onDidChangeActive(async () => { + const xterm = instance.xterm; + if (!xterm) { + return; + } + const [item] = quickPick.activeItems; + if ('command' in item && item.command && item.command.marker) { + if (!terminalScrollStateSaved) { + xterm.markTracker.saveScrollState(); + terminalScrollStateSaved = true; + } + const promptRowCount = item.command.getPromptRowCount(); + const commandRowCount = item.command.getCommandRowCount(); + xterm.markTracker.revealRange({ + start: { + x: 1, + y: item.command.marker.line - (promptRowCount - 1) + 1 + }, + end: { + x: instance.cols, + y: item.command.marker.line + (commandRowCount - 1) + 1 + } + }); + } else { + restoreScrollState(); + } + }); quickPick.onDidAccept(async () => { const result = quickPick.activeItems[0]; let text: string; @@ -271,7 +304,9 @@ export async function showRunRecentQuickPick( if (quickPick.keyMods.alt) { instance.focus(); } + restoreScrollState(); }); + quickPick.onDidHide(() => restoreScrollState()); if (value) { quickPick.value = value; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index e1c940dbeba..bc95338207c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -154,6 +154,8 @@ export class TerminalService extends Disposable implements ITerminalService { get onDidChangeActiveGroup(): Event { return this._onDidChangeActiveGroup.event; } // Lazily initialized events that fire when the specified event fires on _any_ terminal + // TODO: Batch events + @memoize get onAnyInstanceData() { return this.createOnInstanceEvent(instance => Event.map(instance.onData, data => ({ instance, data }))); } @memoize get onAnyInstanceDataInput() { return this.createOnInstanceEvent(e => e.onDidInputData); } @memoize get onAnyInstanceIconChange() { return this.createOnInstanceEvent(e => e.onIconChanged); } @memoize get onAnyInstanceMaximumDimensionsChange() { return this.createOnInstanceEvent(e => Event.map(e.onMaximumDimensionsChanged, () => e, e.store)); } @@ -190,19 +192,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 => { // --- Start Positron --- // Don't hide panel or reset context key when last terminal is hidden. // This prevents hiding the panel along with the Console, which would @@ -220,7 +222,7 @@ export class TerminalService extends Disposable implements ITerminalService { } else if (!instance) { this._terminalShellTypeContextKey.reset(); } - }); + })); this._handleInstanceContextKeys(); this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService); @@ -237,7 +239,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))); @@ -282,7 +284,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'); @@ -529,14 +531,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 { @@ -1286,7 +1288,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/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index e9bf671e021..e2fd9619a69 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -279,9 +279,9 @@ class TerminalTabsRenderer implements IListRenderer + actionViewItemProvider: (action, options) => action instanceof MenuItemAction - ? this._instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) + ? this._instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) : undefined }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index ebd4abf5e19..638ad6e95c1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 31ed968488e..7bf4f427bd0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts b/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts index 9ff52e29fc5..a22ec3f5ddd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalVoice.ts +++ b/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, 'terminal'); this._disposables.add(session.onDidChange((e) => { if (this._cancellationTokenSource?.token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts index 499e1d2f57d..9a2fbf8e6d7 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 0f2ef8f01f4..c304610faa7 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/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/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index fc925b646bb..d2ac113bfbd 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/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/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 0df9dc1171c..bdc76cdf957 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/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: false + }, [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/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 06779c99e21..7f40100cf84 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -37,6 +37,7 @@ import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statu import { memoize } from 'vs/base/common/decorators'; import { StopWatch } from 'vs/base/common/stopwatch'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; export class LocalTerminalBackendContribution implements IWorkbenchContribution { @@ -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/src/vs/workbench/contrib/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index b49aa829f7b..e9cdc253212 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -17,6 +17,7 @@ import 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.acce import 'vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution'; import 'vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution'; import 'vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution'; +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution'; import 'vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution'; import 'vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution'; import 'vs/workbench/contrib/terminalContrib/zoom/browser/terminal.zoom.contribution'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css new file mode 100644 index 00000000000..e515158cd77 --- /dev/null +++ b/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/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts new file mode 100644 index 00000000000..44eabbf13f6 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; +import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; +import { TerminalChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; + +registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); + +registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(TerminalChatAccessibilityHelpContribution.ID, TerminalChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts new file mode 100644 index 00000000000..6a8b0b8854c --- /dev/null +++ b/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/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts new file mode 100644 index 00000000000..584bdc753d8 --- /dev/null +++ b/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/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts new file mode 100644 index 00000000000..f5bbe82de03 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +export class TerminalInlineChatAccessibleViewContribution extends Disposable { + static ID: 'terminalInlineChatAccessibleViewContribution'; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(105, 'terminalInlineChat', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const terminalService = accessor.get(ITerminalService); + const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; + if (!controller?.lastResponseContent) { + return false; + } + const responseContent = controller.lastResponseContent; + accessibleViewService.show({ + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.View } + }); + return true; + }, TerminalChatContextKeys.focused)); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts new file mode 100644 index 00000000000..9ce75d222dc --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -0,0 +1,321 @@ +/*--------------------------------------------------------------------------------------------- + * 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: 'navigation', + 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?.inlineChatWidget.chatWidget.focusLastMessage(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FocusInput, + title: localize2('focusTerminalInput', 'Focus Terminal Input'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.KeyI], + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, CTX_INLINE_CHAT_FOCUSED.toNegated()), + 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: 'navigation', + 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: 'navigation', + 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: 'navigation', + when: TerminalChatContextKeys.requestActive, + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.cancel(); + } +}); + +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(); + } +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts new file mode 100644 index 00000000000..9393e976205 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -0,0 +1,411 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } 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 { 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 _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 _terminalAgentName = 'terminal'; + private _terminalAgentId: string | undefined; + + 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); + + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + + if (!this.initTerminalAgent()) { + this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent())); + } + this._register(this._chatCodeBlockContextProviderService.registerProvider({ + getCodeBlockContext: (editor) => { + if (!editor || !this._chatWidget?.hasValue || !this.hasFocus()) { + return; + } + return { + element: editor, + code: editor.getValue(), + codeBlockIndex: 0, + languageId: editor.getModel()!.getLanguageId() + }; + } + }, 'terminal')); + + // TODO + // This is glue/debt that's needed while ChatModel isn't yet adopted. The chat model uses + // a default chat model (unless configured) and feedback is reported against that one. This + // code forwards the feedback to an actual registered provider + this._register(this._chatService.onDidPerformUserAction(e => { + if (e.providerId === this._chatWidget?.rawValue?.inlineChatWidget.getChatModel().providerId) { + if (e.action.kind === 'bug') { + this.acceptFeedback(undefined); + } else if (e.action.kind === 'vote') { + this.acceptFeedback(e.action.direction === InteractiveSessionVoteDirection.Up); + } + } + })); + } + + private initTerminalAgent(): boolean { + const terminalAgent = this._chatAgentService.getAgentsByName(this._terminalAgentName)[0]; + if (terminalAgent) { + this._terminalAgentId = terminalAgent.id; + this._terminalAgentRegisteredContextKey.set(true); + return true; + } + + return false; + } + + 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?.rawValue?.hide(); + this._chatWidget?.rawValue?.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'); + } + } + this._messages.fire(Message.ACCEPT_INPUT); + 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') { + responseContent += progress.content; + } else if (progress.kind === 'markdownContent') { + responseContent += progress.content.value; + } + if (this._currentRequest) { + model.acceptResponseProgress(this._currentRequest, progress); + } + }; + + await model.waitForInitialization(); + this._chatWidget?.value.addToHistory(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); + } + 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?.rawValue?.hasFocus() ?? false; + } + + async acceptCommand(shouldExecute: boolean): Promise { + const code = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); + if (!code) { + return; + } + this._chatWidget?.value.acceptCommand(code.textEditorModel.getValue(), shouldExecute); + } + + reveal(): void { + this._chatWidget?.value.reveal(); + } + + 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?.rawValue?.hide(); + } + } + + // 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/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts new file mode 100644 index 00000000000..360f6d0d20f --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +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, + ChatAgentLocation.Terminal, + { + 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', + editableCodeBlocks: true + } + ); + 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)); + this.hide(); + } + + private _dimension?: Dimension; + + private _relayout() { + if (this._dimension) { + this._doLayout(this._inlineChatWidget.contentHeight); + } + } + + 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.contentHeight); + 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.contentHeight; + 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._inlineChatWidget.reset(); + this._focusedContextKey.set(false); + this._visibleContextKey.set(false); + this._inlineChatWidget.value = ''; + this._instance.focus(); + } + focus(): void { + this._inlineChatWidget.focus(); + } + hasFocus(): boolean { + return this._inlineChatWidget.hasFocus(); + } + input(): string { + return this._inlineChatWidget.value; + } + addToHistory(input: string): void { + this._inlineChatWidget.addToHistory(input); + this._inlineChatWidget.saveState(); + } + 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; + } +} + diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts index b680864e23d..389f40afc6e 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts @@ -83,6 +83,11 @@ export interface ITerminalSimpleLink { */ uri?: URI; + /** + * An optional full line to be used for context when resolving. + */ + contextLine?: string; + /** * The location or selection range of the link. */ diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts index 0327aa48168..2a0269486a3 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts @@ -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/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts index 4654d703f1d..84d9ca4fdd2 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLink.ts @@ -13,6 +13,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TerminalLinkType } from 'vs/workbench/contrib/terminalContrib/links/browser/links'; import { IHoverAction } from 'vs/platform/hover/browser/hover'; +import type { URI } from 'vs/base/common/uri'; +import type { IParsedLink } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; export class TerminalLink extends DisposableStore implements ILink { decorations: ILinkDecorations; @@ -30,6 +32,8 @@ export class TerminalLink extends DisposableStore implements ILink { private readonly _xterm: Terminal, readonly range: IBufferRange, readonly text: string, + readonly uri: URI | undefined, + readonly parsedLink: IParsedLink | undefined, readonly actions: IHoverAction[] | undefined, private readonly _viewportY: number, private readonly _activateCallback: (event: MouseEvent | undefined, uri: string) => Promise, diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkDetectorAdapter.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkDetectorAdapter.ts index 8226841a9a0..26c047ea42d 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkDetectorAdapter.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkDetectorAdapter.ts @@ -92,9 +92,7 @@ export class TerminalLinkDetectorAdapter extends Disposable implements ILinkProv const detectedLinks = await this._detector.detect(lines, startLine, endLine); for (const link of detectedLinks) { - links.push(this._createTerminalLink(link, async (event) => { - this._onDidActivateLink.fire({ link, event }); - })); + links.push(this._createTerminalLink(link, async (event) => this._onDidActivateLink.fire({ link, event }))); } return links; @@ -110,6 +108,8 @@ export class TerminalLinkDetectorAdapter extends Disposable implements ILinkProv this._detector.xterm, l.bufferRange, l.text, + l.uri, + l.parsedLink, l.actions, this._detector.xterm.buffer.active.viewportY, activateCallback, diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 8d981fb62d2..5d8f803de51 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/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/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index 6b5af707012..d4247bbd835 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts @@ -22,7 +22,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; +import { detectLinks, getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { @@ -98,10 +98,27 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { async open(link: ITerminalSimpleLink): Promise { const osPath = osPathModule(this._getOS()); const pathSeparator = osPath.sep; + // Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format let text = link.text.replace(/^file:\/\/\/?/, ''); text = osPath.normalize(text).replace(/^(\.+[\\/])+/, ''); + // Try extract any trailing line and column numbers by matching the text against parsed + // links. This will give a search link `foo` on a line like `"foo", line 10` to open the + // quick pick with `foo:10` as the contents. + if (link.contextLine) { + const parsedLinks = detectLinks(link.contextLine, this._getOS()); + const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text === parsedLink.path.text); + if (matchingParsedLink) { + if (matchingParsedLink.suffix?.row !== undefined) { + text += `:${matchingParsedLink.suffix.row}`; + if (matchingParsedLink.suffix?.col !== undefined) { + text += `:${matchingParsedLink.suffix.col}`; + } + } + } + } + // Remove `:` from the end of the link. // Examples: // - Ruby stack traces: :in ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts index 8328315b957..94dbbd2f5cd 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts +++ b/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/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts index e954be537df..80a6e670c4a 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts @@ -6,32 +6,56 @@ import { EventType } from 'vs/base/browser/dom'; import { Emitter, Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; -import { QuickPickItem, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +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, 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 { 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: PickerEditorState; + + private _instance: ITerminalInstance | IDetachedTerminalInstance | undefined; + private readonly _onDidRequestMoreLinks = this.add(new Emitter()); readonly onDidRequestMoreLinks = this._onDidRequestMoreLinks.event; constructor( + @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 = 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) { @@ -57,44 +81,72 @@ export class TerminalLinkQuickpick extends DisposableStore { pick.placeholder = localize('terminal.integrated.openDetectedLink', "Select the link to open, type to filter all links"); pick.sortByLabel = false; pick.show(); + if (pick.activeItems.length > 0) { + this._previewItem(pick.activeItems[0]); + } // Show all results only when filtering begins, this is done so the quick pick will show up // 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; + this._previewItem(item); })); return new Promise(r => { - disposables.add(pick.onDidHide(() => { + 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(); + } disposables.dispose(); if (pick.selectedItems.length === 0) { this._accessibleViewService.showLastProvider(AccessibleViewProviderId.Terminal); @@ -102,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]; @@ -117,25 +179,114 @@ 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 (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) { + return; + } + + // Any link can be previewed in the termninal + const link = item.link; + this._previewItemInTerminal(link); + + if (!('uri' in link) || !link.uri) { + return; + } + + 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, + startColumn: linkSuffix.col ?? 1, + endLineNumber: linkSuffix.rowEnd, + endColumn: linkSuffix.colEnd + }; + + this._editorViewState.set(); + this._editorSequencer.queue(async () => { + 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 { - link: ILink; + link: ILink | TerminalLink; } type LinkQuickPickItem = ITerminalLinkQuickPickItem | QuickPickItem; diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts index 259df08a249..adee3ee9f1f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts @@ -37,6 +37,8 @@ const enum Constants { const fallbackMatchers: RegExp[] = [ // Python style error: File "", line /^ *File (?"(?.+)"(, line (?\d+))?)/, + // Unknown tool #200166: FILE :: + /^ +FILE +(?(?.+)(?::(?\d+)(?::(?\d+))?)?)/, // Some C++ compile error formats: // C:\foo\bar baz(339) : error ... // C:\foo\bar baz(339,12) : error ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts index 1675d0fac73..02a9b2cc39f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts @@ -103,7 +103,8 @@ export class TerminalWordLinkDetector extends Disposable implements ITerminalLin links.push({ text: word.text, bufferRange, - type: TerminalBuiltinLinkType.Search + type: TerminalBuiltinLinkType.Search, + contextLine: text }); } diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts index 71d641d0739..04e4aeb5e9b 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts +++ b/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/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index faae5d685ee..785ad9658ab 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -138,6 +138,10 @@ const supportedFallbackLinkFormats: LinkFormatInfo[] = [ // Python style error: File "", line { urlFormat: 'File "{0}"', linkCellStartOffset: 5 }, { urlFormat: 'File "{0}", line {1}', line: '5', linkCellStartOffset: 5 }, + // Unknown tool #200166: FILE :: + { urlFormat: ' FILE {0}', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}', line: '5', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}:{2}', line: '5', column: '3', linkCellStartOffset: 7 }, // Some C++ compile error formats { urlFormat: '{0}({1}) :', line: '5', linkCellEndOffset: -2 }, { urlFormat: '{0}({1},{2}) :', line: '5', column: '3', linkCellEndOffset: -2 }, diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 8868f63beef..f3c1633701d 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/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'; @@ -59,6 +59,7 @@ export class TerminalStickyScrollOverlay extends Disposable { private _refreshListeners = this._register(new MutableDisposable()); private _state: OverlayState = OverlayState.Off; + private _isRefreshQueued = false; private _rawMaxLineCount: number = 5; constructor( @@ -95,6 +96,9 @@ export class TerminalStickyScrollOverlay extends Disposable { // Eagerly create the overlay xtermCtor.then(ctor => { + if (this._store.isDisposed) { + return; + } this._stickyScrollOverlay = this._register(new ctor({ rows: 1, cols: this._xterm.raw.cols, @@ -109,6 +113,10 @@ export class TerminalStickyScrollOverlay extends Disposable { this._register(this._themeService.onDidColorThemeChange(() => { this._syncOptions(); })); + this._register(this._xterm.raw.onResize(() => { + this._syncOptions(); + this._refresh(); + })); this._getSerializeAddonConstructor().then(SerializeAddon => { this._serializeAddon = this._register(new SerializeAddon()); @@ -147,7 +155,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()), ); @@ -168,39 +176,18 @@ export class TerminalStickyScrollOverlay extends Disposable { this._element?.classList.toggle(CssClasses.Visible, isVisible); } - /** - * The entry point to refresh sticky scroll. This is synchronous and will call into the method - * that actually refreshes using either debouncing or throttling depending on the situation. - * - * The goal is that if the command has changed to update immediately (with throttling) and if - * the command is the same then update with debouncing as it's less likely updates will show up. - * This approach also helps with: - * - * - Cursor move only updates such as moving horizontally in pagers which without this may show - * the sticky scroll before hiding it again almost immediately due to everything not being - * parsed yet. - * - Improving performance due to deferring less important updates via debouncing. - * - Less flickering when scrolling, while still updating immediately when the command changes. - */ private _refresh(): void { - if (!this._xterm.raw.element?.parentElement || !this._stickyScrollOverlay || !this._serializeAddon) { + if (this._isRefreshQueued) { return; } - const command = this._commandDetection.getCommandForLine(this._xterm.raw.buffer.active.viewportY); - if (command && this._currentStickyCommand !== command) { - this._throttledRefresh(); - } else { - this._debouncedRefresh(); - } - } - - @debounce(20) - private _debouncedRefresh(): void { - this._throttledRefresh(); + this._isRefreshQueued = true; + queueMicrotask(() => { + this._refreshNow(); + this._isRefreshQueued = false; + }); } - @throttle(0) - private _throttledRefresh(): void { + 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 @@ -242,6 +229,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(); @@ -278,7 +271,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, @@ -294,8 +287,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; @@ -314,7 +312,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); @@ -367,12 +376,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 => { @@ -434,6 +446,7 @@ export class TerminalStickyScrollOverlay extends Disposable { private _getOptions(): ITerminalOptions { const o = this._xterm.raw.options; return { + allowTransparency: true, cursorInactiveStyle: 'none', scrollback: 0, logLevel: 'off', diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 0cf5fabbe4e..f00cc50b073 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { SuggestAddon } from 'vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon'; -import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalConfiguration, ITerminalProcessManager, TERMINAL_CONFIG_SECTION, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { ContextKeyExpr, IContextKey, IContextKeyService, IReadableSet } from 'vs/platform/contextkey/common/contextkey'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -18,6 +18,8 @@ import { registerActiveInstanceAction } from 'vs/workbench/contrib/terminal/brow import { localize2 } from 'vs/nls'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; class TerminalSuggestContribution extends DisposableStore implements ITerminalContribution { static readonly ID = 'terminal.suggest'; @@ -26,17 +28,18 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo return instance.getContribution(TerminalSuggestContribution.ID); } - private _addon: SuggestAddon | undefined; + private _addon: MutableDisposable = new MutableDisposable(); private _terminalSuggestWidgetContextKeys: IReadableSet = new Set(TerminalContextKeys.suggestWidgetVisible.key); private _terminalSuggestWidgetVisibleContextKey: IContextKey; - get addon(): SuggestAddon | undefined { return this._addon; } + get addon(): SuggestAddon | undefined { return this._addon.value; } constructor( private readonly _instance: ITerminalInstance, _processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -51,21 +54,31 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo this._loadSuggestAddon(xterm.raw); } })); + this.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalSettingId.SendKeybindingsToShell)) { + this._loadSuggestAddon(xterm.raw); + } + })); } private _loadSuggestAddon(xterm: RawXtermTerminal): void { + const sendingKeybindingsToShell = this._configurationService.getValue(TERMINAL_CONFIG_SECTION).sendKeybindingsToShell; + if (sendingKeybindingsToShell) { + this._addon.dispose(); + return; + } if (this._terminalSuggestWidgetVisibleContextKey) { - this._addon = this._instantiationService.createInstance(SuggestAddon, this._terminalSuggestWidgetVisibleContextKey); - xterm.loadAddon(this._addon); - this._addon?.setPanel(dom.findParentWithClass(xterm.element!, 'panel')!); - this._addon?.setScreen(xterm.element!.querySelector('.xterm-screen')!); - this.add(this._instance.onDidBlur(() => this._addon?.hideSuggestWidget())); - this.add(this._addon.onAcceptedCompletion(async text => { + this._addon.value = this._instantiationService.createInstance(SuggestAddon, this._terminalSuggestWidgetVisibleContextKey); + xterm.loadAddon(this._addon.value); + this._addon.value.setPanel(dom.findParentWithClass(xterm.element!, 'panel')!); + this._addon.value.setScreen(xterm.element!.querySelector('.xterm-screen')!); + this.add(this._instance.onDidBlur(() => this._addon.value?.hideSuggestWidget())); + this.add(this._addon.value.onAcceptedCompletion(async text => { this._instance.focus(); this._instance.sendText(text, false); })); this.add(this._instance.onDidSendText((text) => { - this._addon?.handleNonXtermData(text); + this._addon.value?.handleNonXtermData(text); })); } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 46ca6bda9d6..622bebb1d0b 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -425,9 +425,12 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // Right if (data === '\x1b[C') { - handled = true; - this._cursorIndexDelta += 1; - handledCursorDelta++; + // If right requests beyond where the completion was requested (potentially accepting a shell completion), hide + if (this._additionalInput?.length !== this._cursorIndexDelta) { + handled = true; + this._cursorIndexDelta++; + handledCursorDelta++; + } } if (data.match(/^[a-z0-9]$/i)) { diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index d96710fbd0d..8b310a5d71b 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -6,6 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; import { mapFindFirst } from 'vs/base/common/arraysFind'; +import { assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -31,7 +32,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons'; import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; -import { CoverageDetails, DetailType, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; const MAX_HOVERED_LINES = 30; @@ -81,7 +82,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 => { @@ -446,32 +453,42 @@ export class CoverageDetailsModel { /** Gets the markdown description for the given detail */ public describe(detail: CoverageDetailsWithBranch, model: ITextModel): IMarkdownString | undefined { if (detail.type === DetailType.Declaration) { - return new MarkdownString().appendMarkdown(localize('coverage.declExecutedCount', '`{0}` was executed {1} time(s).', detail.name, detail.count)); + return namedDetailLabel(detail.name, detail); } else if (detail.type === DetailType.Statement) { const text = wrapName(model.getValueInRange(tidyLocation(detail.location)).trim() || ``); - const str = new MarkdownString(); if (detail.branches?.length) { const covered = detail.branches.filter(b => !!b.count).length; - str.appendMarkdown(localize('coverage.branches', '{0} of {1} of branches in {2} were covered.', covered, detail.branches.length, text)); + return new MarkdownString().appendMarkdown(localize('coverage.branches', '{0} of {1} of branches in {2} were covered.', covered, detail.branches.length, text)); } else { - str.appendMarkdown(localize('coverage.codeExecutedCount', '{0} was executed {1} time(s).', text, detail.count)); + return namedDetailLabel(text, detail); } - return str; } else if (detail.type === DetailType.Branch) { const text = wrapName(model.getValueInRange(tidyLocation(detail.detail.location)).trim() || ``); const { count, label } = detail.detail.branches![detail.branch]; const label2 = label ? wrapInBackticks(label) : `#${detail.branch + 1}`; - if (count === 0) { + if (!count) { return new MarkdownString().appendMarkdown(localize('coverage.branchNotCovered', 'Branch {0} in {1} was not covered.', label2, text)); + } else if (count === true) { + return new MarkdownString().appendMarkdown(localize('coverage.branchCoveredYes', 'Branch {0} in {1} was executed.', label2, text)); } else { return new MarkdownString().appendMarkdown(localize('coverage.branchCovered', 'Branch {0} in {1} was executed {2} time(s).', label2, text, count)); } } - return undefined; + assertNever(detail); } } +function namedDetailLabel(name: string, detail: IStatementCoverage | IDeclarationCoverage) { + return new MarkdownString().appendMarkdown( + !detail.count // 0 or false + ? localize('coverage.declExecutedNo', '`{0}` was not executed.', name) + : typeof detail.count === 'number' + ? localize('coverage.declExecutedCount', '`{0}` was executed {1} time(s).', name, detail.count) + : localize('coverage.declExecutedYes', '`{0}` was executed.', name) + ); +} + // 'tidies' the range by normalizing it into a range and removing leading // and trailing whitespace. function tidyLocation(location: Range | Position): Range { diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts index d7f385457ae..33060d117ea 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts +++ b/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/src/vs/workbench/contrib/testing/browser/icons.ts b/src/vs/workbench/contrib/testing/browser/icons.ts index 81b61817d3f..428ba987914 100644 --- a/src/vs/workbench/contrib/testing/browser/icons.ts +++ b/src/vs/workbench/contrib/testing/browser/icons.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { registerIcon, spinningLoading } from 'vs/platform/theme/common/iconRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; -import { testingColorRunAction, testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme'; +import { testingColorRunAction, testStatesToIconColors, testStatesToRetiredIconColors } from 'vs/workbench/contrib/testing/browser/theme'; import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.')); @@ -52,12 +52,22 @@ export const testingStatesToIcons = new Map([ registerThemingParticipant((theme, collector) => { for (const [state, icon] of testingStatesToIcons.entries()) { const color = testStatesToIconColors[state]; + const retiredColor = testStatesToRetiredIconColors[state]; if (!color) { continue; } collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icon)} { color: ${theme.getColor(color)} !important; }`); + if (!retiredColor) { + continue; + } + collector.addRule(` + .test-explorer .computed-state.retired${ThemeIcon.asCSSSelector(icon)}, + .testing-run-glyph.retired${ThemeIcon.asCSSSelector(icon)}{ + color: ${theme.getColor(retiredColor)} !important; + } + `); } collector.addRule(` diff --git a/src/vs/workbench/contrib/testing/browser/media/testMessageColorizer.css b/src/vs/workbench/contrib/testing/browser/media/testMessageColorizer.css new file mode 100644 index 00000000000..8c3dbab4162 --- /dev/null +++ b/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/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 010fd1c5c75..fc0cecc4d60 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -147,11 +147,6 @@ margin-right: 0.25em; } -.test-explorer .computed-state.retired, -.testing-run-glyph.retired { - opacity: 0.7 !important; -} - .test-explorer .test-is-hidden { opacity: 0.8; } diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts index 7261e5e1021..491abdc854b 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts +++ b/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'; @@ -21,7 +21,7 @@ import { IExplorerFileContribution } from 'vs/workbench/contrib/files/browser/ex import { ITestingCoverageBarThresholds, TestingConfigKeys, TestingDisplayedCoveragePercent, getTestingConfiguration, observeTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; import { AbstractFileCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; -import { ICoveredCount } from 'vs/workbench/contrib/testing/common/testTypes'; +import { ICoverageCount } from 'vs/workbench/contrib/testing/common/testTypes'; export interface TestCoverageBarsOptions { /** @@ -128,7 +128,7 @@ export class ManagedTestCoverageBars extends Disposable { } } -const percent = (cc: ICoveredCount) => clamp(cc.total === 0 ? 1 : cc.covered / cc.total, 0, 1); +const percent = (cc: ICoverageCount) => clamp(cc.total === 0 ? 1 : cc.covered / cc.total, 0, 1); const epsilon = 10e-8; const barWidth = 16; diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index 2451613400f..1903783b359 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/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,9 +43,10 @@ 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'; +import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const enum CoverageSortOrder { @@ -151,8 +153,8 @@ class DeclarationCoverageNode { return; } - const statement: ICoveredCount = { covered: 0, total: 0 }; - const branch: ICoveredCount = { covered: 0, total: 0 }; + const statement: ICoverageCount = { covered: 0, total: 0 }; + const branch: ICoverageCount = { covered: 0, total: 0 }; for (const detail of this.containedDetails) { if (detail.type !== DetailType.Statement) { continue; @@ -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/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index f6f451bcd0d..103efb04a9c 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testMessageColorizer.ts b/src/vs/workbench/contrib/testing/browser/testMessageColorizer.ts new file mode 100644 index 00000000000..527d6e5cc32 --- /dev/null +++ b/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/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 6083c9af896..48f10e9210a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index ba9050a610b..16b2f5f1e92 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { Action, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; @@ -46,11 +46,12 @@ export class TestingExplorerFilter extends BaseActionViewItem { constructor( action: IAction, + options: IBaseActionViewItemOptions, @ITestExplorerFilterState private readonly state: ITestExplorerFilterState, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITestService private readonly testService: ITestService, ) { - super(null, action); + super(null, action, options); this.updateFilterActiveState(); this._register(testService.excluded.onTestExclusionsChanged(this.updateFilterActiveState, this)); } @@ -120,9 +121,9 @@ export class TestingExplorerFilter extends BaseActionViewItem { }))); const actionbar = this._register(new ActionBar(container, { - actionViewItemProvider: action => { + actionViewItemProvider: (action, options) => { if (action.id === this.filtersAction.id) { - return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.state, this.actionRunner); + return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, options, this.state, this.actionRunner); } return undefined; }, @@ -175,6 +176,7 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { constructor( action: IAction, + options: IActionViewItemOptions, private readonly filters: ITestExplorerFilterState, actionRunner: IActionRunner, @IContextMenuService contextMenuService: IContextMenuService, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index ba7f2800830..e5e8e34abfa 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -5,8 +5,11 @@ import * as dom from 'vs/base/browser/dom'; 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/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'; @@ -285,18 +288,18 @@ export class TestingExplorerView extends ViewPane { } /** @override */ - public override getActionViewItem(action: IAction): IActionViewItem | undefined { + public override getActionViewItem(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { switch (action.id) { case TestCommandId.FilterAction: - this.filter.value = this.instantiationService.createInstance(TestingExplorerFilter, action); + this.filter.value = this.instantiationService.createInstance(TestingExplorerFilter, action, options); this.filterFocusListener.value = this.filter.value.onDidFocus(() => this.lastFocusState = LastFocusState.Input); return this.filter.value; case TestCommandId.RunSelectedAction: - return this.getRunGroupDropdown(TestRunProfileBitset.Run, action); + return this.getRunGroupDropdown(TestRunProfileBitset.Run, action, options); case TestCommandId.DebugSelectedAction: - return this.getRunGroupDropdown(TestRunProfileBitset.Debug, action); + return this.getRunGroupDropdown(TestRunProfileBitset.Debug, action, options); default: - return super.getActionViewItem(action); + return super.getActionViewItem(action, options); } } @@ -380,10 +383,10 @@ export class TestingExplorerView extends ViewPane { super.saveState(); } - private getRunGroupDropdown(group: TestRunProfileBitset, defaultAction: IAction) { + private getRunGroupDropdown(group: TestRunProfileBitset, defaultAction: IAction, options: IActionViewItemOptions) { const dropdownActions = this.getTestConfigGroupActions(group); if (dropdownActions.length < 2) { - return super.getActionViewItem(defaultAction); + return super.getActionViewItem(defaultAction, options); } const primaryAction = this.instantiationService.createInstance(MenuItemAction, { @@ -401,13 +404,13 @@ export class TestingExplorerView extends ViewPane { primaryAction, dropdownAction, dropdownActions, '', this.contextMenuService, - {} + options ); } private createFilterActionBar() { const bar = new ActionBar(this.treeHeader, { - actionViewItemProvider: action => this.getActionViewItem(action), + actionViewItemProvider: (action, options) => this.getActionViewItem(action, options), triggerKeys: { keyDown: false, keys: [] }, }); bar.push(new Action(TestCommandId.FilterAction)); @@ -442,6 +445,7 @@ class ResultSummaryView extends Disposable { private elementsWereAttached = false; private badgeType: TestingCountBadge; private lastBadge?: NumberBadge | IconBadge; + private countHover: ICustomHover; private readonly badgeDisposable = this._register(new MutableDisposable()); private readonly renderLoop = this._register(new RunOnceScheduler(() => this.render(), SUMMARY_RENDER_INTERVAL)); private readonly elements = dom.h('div.result-summary', [ @@ -472,6 +476,8 @@ class ResultSummaryView extends Disposable { } })); + this.countHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.elements.count, '')); + const ab = this._register(new ActionBar(this.elements.rerun, { actionViewItemProvider: (action, options) => createActionViewItem(instantiationService, action, options), })); @@ -518,7 +524,7 @@ class ResultSummaryView extends Disposable { } count.textContent = `${counts.passed}/${counts.totalWillBeRun}`; - count.title = getTestProgressText(counts); + this.countHover.update(getTestProgressText(counts)); this.renderActivityBadge(counts); if (!this.elementsWereAttached) { @@ -1301,6 +1307,7 @@ class IdentityProvider implements IIdentityProvider { interface IErrorTemplateData { label: HTMLElement; + disposable: DisposableStore; } class ErrorRenderer implements ITreeRenderer { @@ -1318,7 +1325,7 @@ class ErrorRenderer implements ITreeRenderer, _: number, data: IErrorTemplateData): void { @@ -1330,12 +1337,11 @@ class ErrorRenderer implements ITreeRenderer + actionViewItemProvider: (action, options) => action instanceof MenuItemAction - ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) + ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) : undefined })); @@ -1451,7 +1457,7 @@ class TestItemRenderer extends Disposable data.icon.className += ' retired'; } - data.label.title = getLabelForTestTreeElement(node.element); + data.elementDisposable.add(setupCustomHover(getDefaultHoverDelegate('mouse'), data.label, getLabelForTestTreeElement(node.element))); if (node.element.test.item.label.trim()) { dom.reset(data.label, ...renderLabelWithIcons(node.element.test.item.label)); } else { diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index a64fc0274f3..1ecafa2cf8e 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/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'; @@ -37,16 +36,17 @@ import 'vs/css!./testingOutputPeek'; import { ICodeEditor, IDiffEditorConstructionOptions, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +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 { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; +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'; @@ -79,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'; @@ -97,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; @@ -111,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); @@ -120,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; @@ -139,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); } } @@ -590,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); @@ -1037,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')); } @@ -1317,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; @@ -1362,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) { @@ -1744,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; @@ -1825,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, @@ -1853,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) { @@ -1941,6 +1947,7 @@ class OutputPeekTree extends Disposable { result = Iterable.concat( Iterable.single>({ element: new CoverageElement(results, task, coverageService), + incompressible: true, }), result, ); @@ -2226,9 +2233,9 @@ class TestRunElementRenderer implements ICompressibleTreeRenderer + actionViewItemProvider: (action, options) => action instanceof MenuItemAction - ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) + ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) : undefined }); @@ -2356,11 +2363,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( @@ -2400,12 +2406,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/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 4e232405411..536c03da5f9 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -190,6 +190,57 @@ export const testStatesToIconColors: { [K in TestResultState]?: string } = { [TestResultState.Skipped]: testingColorIconSkipped, }; +export const testingRetiredColorIconErrored = registerColor('testing.iconErrored.retired', { + dark: transparent(testingColorIconErrored, 0.7), + light: transparent(testingColorIconErrored, 0.7), + hcDark: transparent(testingColorIconErrored, 0.7), + hcLight: transparent(testingColorIconErrored, 0.7) +}, localize('testing.iconErrored.retired', "Retired color for the 'Errored' icon in the test explorer.")); + +export const testingRetiredColorIconFailed = registerColor('testing.iconFailed.retired', { + dark: transparent(testingColorIconFailed, 0.7), + light: transparent(testingColorIconFailed, 0.7), + hcDark: transparent(testingColorIconFailed, 0.7), + hcLight: transparent(testingColorIconFailed, 0.7) +}, localize('testing.iconFailed.retired', "Retired color for the 'failed' icon in the test explorer.")); + +export const testingRetiredColorIconPassed = registerColor('testing.iconPassed.retired', { + dark: transparent(testingColorIconPassed, 0.7), + light: transparent(testingColorIconPassed, 0.7), + hcDark: transparent(testingColorIconPassed, 0.7), + hcLight: transparent(testingColorIconPassed, 0.7) +}, localize('testing.iconPassed.retired', "Retired color for the 'passed' icon in the test explorer.")); + +export const testingRetiredColorIconQueued = registerColor('testing.iconQueued.retired', { + dark: transparent(testingColorIconQueued, 0.7), + light: transparent(testingColorIconQueued, 0.7), + hcDark: transparent(testingColorIconQueued, 0.7), + hcLight: transparent(testingColorIconQueued, 0.7) +}, localize('testing.iconQueued.retired', "Retired color for the 'Queued' icon in the test explorer.")); + +export const testingRetiredColorIconUnset = registerColor('testing.iconUnset.retired', { + dark: transparent(testingColorIconUnset, 0.7), + light: transparent(testingColorIconUnset, 0.7), + hcDark: transparent(testingColorIconUnset, 0.7), + hcLight: transparent(testingColorIconUnset, 0.7) +}, localize('testing.iconUnset.retired', "Retired color for the 'Unset' icon in the test explorer.")); + +export const testingRetiredColorIconSkipped = registerColor('testing.iconSkipped.retired', { + dark: transparent(testingColorIconSkipped, 0.7), + light: transparent(testingColorIconSkipped, 0.7), + hcDark: transparent(testingColorIconSkipped, 0.7), + hcLight: transparent(testingColorIconSkipped, 0.7) +}, localize('testing.iconSkipped.retired', "Retired color for the 'Skipped' icon in the test explorer.")); + +export const testStatesToRetiredIconColors: { [K in TestResultState]?: string } = { + [TestResultState.Errored]: testingRetiredColorIconErrored, + [TestResultState.Failed]: testingRetiredColorIconFailed, + [TestResultState.Passed]: testingRetiredColorIconPassed, + [TestResultState.Queued]: testingRetiredColorIconQueued, + [TestResultState.Unset]: testingRetiredColorIconUnset, + [TestResultState.Skipped]: testingRetiredColorIconSkipped, +}; + registerThemingParticipant((theme, collector) => { const editorBg = theme.getColor(editorBackground); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 35d9eb5e552..8f6bb09e32a 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/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/src/vs/workbench/contrib/testing/common/observableUtils.ts b/src/vs/workbench/contrib/testing/common/observableUtils.ts new file mode 100644 index 00000000000..26c6c087d78 --- /dev/null +++ b/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/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 05d532263da..10d4f23cea3 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/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'; +import { CoverageDetails, ICoverageCount, 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) { + 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; @@ -125,7 +113,7 @@ export class TestCoverage { } } -export const getTotalCoveragePercent = (statement: ICoveredCount, branch: ICoveredCount | undefined, function_: ICoveredCount | undefined) => { +export const getTotalCoveragePercent = (statement: ICoverageCount, branch: ICoverageCount | undefined, function_: ICoverageCount | undefined) => { let numerator = statement.covered; let denominator = statement.total; @@ -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: ICoverageCount; + public branch?: ICoverageCount; + public declaration?: ICoverageCount; + 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/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 57c0832fdfd..0bf62937458 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/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/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index e6056621bf2..6bbff4a7c94 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/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/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 7737ef78020..9aa9db27bcb 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/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, @@ -532,50 +542,49 @@ export interface ITestCoverage { files: IFileCoverage[]; } -export interface ICoveredCount { +export interface ICoverageCount { covered: number; total: number; } -export namespace ICoveredCount { - export const empty = (): ICoveredCount => ({ covered: 0, total: 0 }); - export const sum = (target: ICoveredCount, src: Readonly) => { +export namespace ICoverageCount { + export const empty = (): ICoverageCount => ({ covered: 0, total: 0 }); + export const sum = (target: ICoverageCount, src: Readonly) => { target.covered += src.covered; target.total += src.total; }; } export interface IFileCoverage { + id: string; uri: URI; - statement: ICoveredCount; - branch?: ICoveredCount; - declaration?: ICoveredCount; - details?: CoverageDetails[]; + statement: ICoverageCount; + branch?: ICoverageCount; + declaration?: ICoverageCount; } - export namespace IFileCoverage { export interface Serialized { + id: string; uri: UriComponents; - statement: ICoveredCount; - branch?: ICoveredCount; - declaration?: ICoveredCount; - details?: CoverageDetails.Serialized[]; + statement: ICoverageCount; + branch?: ICoverageCount; + declaration?: ICoverageCount; } 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/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index 2509bbd2c12..8f4dab17dc1 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/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/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index cc7821a4e27..ddef4fcdc15 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/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/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts similarity index 100% rename from src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts rename to src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts similarity index 100% rename from src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts rename to src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index 569661d627d..e0923164c97 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/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/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 06476f564c8..87503bb990c 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/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/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts index 8753d7436f5..d5bf22e671a 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts @@ -15,7 +15,7 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index b5e2de0ed52..9f6cc07ba5f 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/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/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 969c563d5de..af1b3d507c2 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/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/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index e86a7c07e96..e213af65498 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/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/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 0787368edba..08c30bd1f30 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -918,7 +918,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: syncNowCommand.id, label: `${SYNC_TITLE.value}: ${syncNowCommand.title.original}`, description: syncNowCommand.description(that.userDataSyncService) }); if (that.userDataSyncEnablementService.canToggleEnablement()) { const account = that.userDataSyncWorkbenchService.current; - items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getLabel(account.authenticationProviderId)})` : undefined }); + items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getProvider(account.authenticationProviderId).label})` : undefined }); } quickPick.items = items; disposables.add(quickPick.onDidAccept(() => { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 248d8c0756f..e81df189fa1 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/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/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index 8c0ea617f8e..2e20edf9bfc 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/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'; // --- Start Positron --- @@ -40,6 +41,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; @@ -53,7 +57,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 ) { @@ -107,21 +111,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); @@ -172,6 +187,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; @@ -188,7 +219,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { } } - private _show() { + private _show(targetWindow: CodeWindow) { if (this._isDisposed) { throw new Error('OverlayWebview is disposed'); } @@ -219,7 +250,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/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 8b22da14204..5e094fc4ccc 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/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/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 277a619ddc7..fa7b15e39c8 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-bQPwjO6bLiyf6v9eDVtAI67LrfonA1w49aFkRXBy4/g=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> { - 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/src/vs/workbench/contrib/webview/browser/themeing.ts b/src/vs/workbench/contrib/webview/browser/themeing.ts index 4fd074d18ae..b63bfffca2d 100644 --- a/src/vs/workbench/contrib/webview/browser/themeing.ts +++ b/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/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index a56aaaa21bf..efa290ff9be 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -83,7 +83,6 @@ export interface WebviewInitInfo { readonly contentOptions: WebviewContentOptions; readonly extension: WebviewExtensionDescription | undefined; - readonly codeWindow?: CodeWindow; } export const enum WebviewContentPurpose { @@ -290,7 +289,7 @@ export interface IWebviewElement extends IWebview { * * @param parent Element to append the webview to. */ - mountTo(parent: HTMLElement): void; + mountTo(parent: HTMLElement, targetWindow: CodeWindow): void; } /** @@ -320,7 +319,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/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index a2aa9263f0c..a91c79f75dd 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener, EventType, getActiveWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, 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'; // --- Start Positron --- // eslint-disable-next-line no-duplicate-imports @@ -93,7 +93,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'; } @@ -108,7 +111,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 `