diff --git a/.devcontainer/install-vscode.sh b/.devcontainer/install-vscode.sh index 9d4b52755d9d2..cc70d527acdfb 100755 --- a/.devcontainer/install-vscode.sh +++ b/.devcontainer/install-vscode.sh @@ -9,4 +9,4 @@ sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.mi rm -f packages.microsoft.gpg apt update -apt install -y code-insiders libsecret-1-dev libxkbfile-dev +apt install -y code-insiders libsecret-1-dev libxkbfile-dev libkrb5-dev diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac5f9e073b79..8a4d93dfa770c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: - name: Setup Build Environment run: | sudo apt-get update - sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 + sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libkrb5-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 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 diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index a86f94bc331f9..46ece33df5c7c 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -45,6 +45,9 @@ jobs: path: ${{ steps.yarnCacheDirPath.outputs.dir }} key: ${{ runner.os }}-yarnCacheDir-${{ steps.nodeModulesCacheKey.outputs.value }} restore-keys: ${{ runner.os }}-yarnCacheDir- + - name: Install libkrb5-dev + if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} + run: sudo apt install -y libkrb5-dev - name: Execute yarn if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} env: diff --git a/.yarnrc b/.yarnrc index 3af2059dbd78d..4b56fb47de5f1 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "22.3.14" -ms_build_id "21893604" +target "22.3.17" +ms_build_id "22432899" runtime "electron" build_from_source "true" diff --git a/build/.cachesalt b/build/.cachesalt index d63bdc3118947..26ad5de2bcab8 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2023-06-12T12:55:48.130Z +2023-07-20T13:31:34.746Z diff --git a/build/.moduleignore b/build/.moduleignore index abc37e3138c4c..76942a81a0582 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -73,6 +73,12 @@ windows-foreground-love/build/** windows-foreground-love/src/** !windows-foreground-love/**/*.node +kerberos/binding.gyp +kerberos/build/** +kerberos/src/** +kerberos/node_modules/** +!kerberos/**/*.node + keytar/binding.gyp keytar/build/** keytar/src/** diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index eed3623addcf1..44af6a76985d2 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -3,10 +3,6 @@ steps: inputs: versionSpec: "16.x" - - script: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - displayName: "Register Docker QEMU" - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'arm64')) - - template: ../distro/download-distro.yml - task: AzureKeyVault@1 @@ -67,6 +63,10 @@ steps: displayName: "Pull image" condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: sudo apt-get update && sudo apt-get install -y libkrb5-dev + displayName: Install build dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: | set -e for i in {1..5}; do # try 5 times @@ -78,10 +78,12 @@ steps: echo "Yarn failed $i, trying again..." done env: + npm_config_arch: $(NPM_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) + VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 955b19749a61f..1ca8c9ec1a9ec 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -13,6 +13,7 @@ steps: env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: diff --git a/build/azure-pipelines/linux/install.sh b/build/azure-pipelines/linux/install.sh new file mode 100755 index 0000000000000..b8960fc5fd41d --- /dev/null +++ b/build/azure-pipelines/linux/install.sh @@ -0,0 +1,40 @@ +#!/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" + +if [ -z "$CC" ] || [ -z "$CXX" ]; then + # Download clang based on chromium revision used by vscode + curl -s https://raw.githubusercontent.com/chromium/chromium/108.0.5359.215/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/108.0.5359.215:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/108.0.5359.215:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/108.0.5359.215:build/config/c++/BUILD.gn + export CC=$PWD/.build/CR_Clang/bin/clang + export CXX=$PWD/.build/CR_Clang/bin/clang++ + 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" + export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -Wl,--lto-O0" + export VSCODE_REMOTE_CC=$(which gcc) + export VSCODE_REMOTE_CXX=$(which g++) +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-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index 7532c43c7c9b6..bb75be37e4b5b 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -13,17 +13,7 @@ steps: env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Electron and Playwright - - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - script: | - set -e - sudo apt-get update - sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 - 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 - displayName: Setup build environment + retryCountOnTaskFailure: 3 - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 99bbd4ca6bd98..bf408683d9da9 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -41,15 +41,28 @@ steps: - script: tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz displayName: Extract compilation output - - script: | - set -e - # Start X server - /etc/init.d/xvfb start - # Start dbus session - 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 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + - script: | + set -e + # Start X server + sudo apt-get update + sudo apt-get install -y pkg-config \ + libxss1 \ + dbus \ + xvfb \ + libgtk-3-0 \ + libgbm1 \ + libxkbfile-dev \ + libsecret-1-dev \ + libkrb5-dev + 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')) @@ -72,7 +85,10 @@ steps: - script: | set -e npm config set registry "$NPM_REGISTRY" --location=project - npm config set always-auth=true --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.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM & Yarn @@ -83,17 +99,6 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - # TODO@joaomoreno TODO@deepak1556 this should be part of the base image - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - script: | - sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg - sudo mkdir -m 0755 -p /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg - echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt update && sudo apt install -y docker-ce-cli - displayName: Install Docker client - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - ${{ if and(ne(parameters.VSCODE_QUALITY, 'oss'), or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64'))) }}: - task: Docker@1 displayName: "Pull Docker image" @@ -105,69 +110,65 @@ steps: containerCommand: uname condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - ${{ if and(ne(parameters.VSCODE_QUALITY, 'oss'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - script: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - displayName: Register Docker QEMU - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['VSCODE_ARCH'], 'arm64')) - - - script: | - set -e + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e - 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 - - if [ -z "$CC" ] || [ -z "$CXX" ]; then - # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/108.0.5359.215/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_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/108.0.5359.215:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/108.0.5359.215:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/108.0.5359.215:build/config/c++/BUILD.gn - export CC=$PWD/.build/CR_Clang/bin/clang - export CXX=$PWD/.build/CR_Clang/bin/clang++ - 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" - export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -Wl,--lto-O0" - export VSCODE_REMOTE_CC=$(which gcc) - export VSCODE_REMOTE_CXX=$(which g++) - 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 - env: - npm_config_arch: $(NPM_ARCH) - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - ${{ if and(ne(parameters.VSCODE_QUALITY, 'oss'), 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) - displayName: Install dependencies - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + 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 + + docker run -e GITHUB_TOKEN -e npm_config_arch -e NPM_REGISTRY \ + -e VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME -e VSCODE_HOST_MOUNT \ + -e ELECTRON_SKIP_BINARY_DOWNLOAD -e PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD \ + -v /mnt/vss/_work/1/s:/home/builduser/vscode -v /mnt/vss/_work/1/s/.build/.netrc:/home/builduser/.netrc \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -u 1000:1000 \ + -w /home/builduser/vscode vscodehub.azurecr.io/vscode-linux-build-agent:bionic-$(VSCODE_ARCH) \ + /bin/bash -c "./build/azure-pipelines/linux/install.sh" + + sudo chown -R $USER:$USER /mnt/vss/_work/1/s + env: + npm_config_arch: $(NPM_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) + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: node build/azure-pipelines/distro/mixin-npm condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules + - ${{ else }}: + - script: | + set -e + + 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) + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: | set -e node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt @@ -248,12 +249,32 @@ steps: mv $(Build.ArtifactStagingDirectory)/cli/$APP_NAME $(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)/bin/$CLI_APP_NAME displayName: Make CLI executable + - script: | + set -e + docker run -v /mnt/vss/_work/1/s:/home/builduser/vscode \ + -v /mnt/vss/_work/1/s/.build/.netrc:/home/builduser/.netrc \ + -v /mnt/vss/_work/1/VSCode-linux-$(VSCODE_ARCH):/home/builduser/VSCode-linux-$(VSCODE_ARCH) \ + -u 1000:1000 \ + -w /home/builduser/vscode vscodehub.azurecr.io/vscode-linux-build-agent:bionic-$(VSCODE_ARCH) \ + yarn gulp "vscode-linux-$(VSCODE_ARCH)-prepare-deb" + displayName: Prepare deb package + - script: | set -e yarn gulp "vscode-linux-$(VSCODE_ARCH)-build-deb" echo "##vso[task.setvariable variable=DEB_PATH]$(ls .build/linux/deb/*/deb/*.deb)" displayName: Build deb package + - script: | + set -e + docker run -v /mnt/vss/_work/1/s:/home/builduser/vscode \ + -v /mnt/vss/_work/1/s/.build/.netrc:/home/builduser/.netrc \ + -v /mnt/vss/_work/1/VSCode-linux-$(VSCODE_ARCH):/home/builduser/VSCode-linux-$(VSCODE_ARCH) \ + -u 1000:1000 \ + -w /home/builduser/vscode vscodehub.azurecr.io/vscode-linux-build-agent:bionic-$(VSCODE_ARCH) \ + yarn gulp "vscode-linux-$(VSCODE_ARCH)-prepare-rpm" + displayName: Prepare rpm package + - script: | set -e yarn gulp "vscode-linux-$(VSCODE_ARCH)-build-rpm" diff --git a/build/azure-pipelines/oss/product-build-pr-cache-linux.yml b/build/azure-pipelines/oss/product-build-pr-cache-linux.yml index 97eba56abc3c7..97c5ddd78ffb2 100644 --- a/build/azure-pipelines/oss/product-build-pr-cache-linux.yml +++ b/build/azure-pipelines/oss/product-build-pr-cache-linux.yml @@ -39,6 +39,10 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication + - script: sudo apt-get update && sudo apt-get install -y libkrb5-dev + displayName: Install build dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: | set -e for i in {1..5}; do # try 5 times diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 1be2db3b5fa3c..2d4ad8aa84a0e 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -143,18 +143,6 @@ name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.VSCODE_QUALITY }})" resources: containers: - - container: vscode-bionic-x64 - image: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-x64 - endpoint: VSCodeHub - options: --user 0:0 --cap-add SYS_ADMIN - - container: vscode-arm64 - image: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-arm64 - endpoint: VSCodeHub - options: --user 0:0 --cap-add SYS_ADMIN - - container: vscode-armhf - image: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-armhf - endpoint: VSCodeHub - options: --user 0:0 --cap-add SYS_ADMIN - container: snapcraft image: vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64 endpoint: VSCodeHub @@ -382,7 +370,6 @@ stages: - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - job: Linuxx64UnitTest displayName: Unit Tests - container: vscode-bionic-x64 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -398,7 +385,6 @@ stages: VSCODE_RUN_SMOKE_TESTS: false - job: Linuxx64IntegrationTest displayName: Integration Tests - container: vscode-bionic-x64 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -414,7 +400,6 @@ stages: VSCODE_RUN_SMOKE_TESTS: false - job: Linuxx64SmokeTest displayName: Smoke Tests - container: vscode-bionic-x64 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -431,7 +416,6 @@ stages: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: - job: Linuxx64 - container: vscode-bionic-x64 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -458,7 +442,6 @@ stages: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true)) }}: - job: LinuxArmhf - container: vscode-armhf variables: VSCODE_ARCH: armhf NPM_ARCH: arm @@ -474,7 +457,6 @@ stages: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: - job: LinuxArm64 - container: vscode-arm64 variables: VSCODE_ARCH: arm64 NPM_ARCH: arm64 @@ -499,6 +481,7 @@ stages: - job: LinuxAlpine variables: VSCODE_ARCH: x64 + NPM_ARCH: x64 steps: - template: alpine/product-build-alpine.yml @@ -507,6 +490,7 @@ stages: timeoutInMinutes: 120 variables: VSCODE_ARCH: arm64 + NPM_ARCH: arm64 steps: - template: alpine/product-build-alpine.yml diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 8471cfdec3dea..d83852784744a 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -49,7 +49,7 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication - - script: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libsecret-1-dev libnotify-bin + - script: sudo apt update -y && sudo apt install -y build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libsecret-1-dev libnotify-bin libkrb5-dev displayName: Install build tools condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index a941ae9adfe66..95d1b6afefd03 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -53,6 +53,10 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication + - script: sudo apt-get update && sudo apt-get install -y libkrb5-dev + displayName: Install build dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: | set -e for i in {1..5}; do # try 5 times diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index 630e226a742ac..3a24e95657ad6 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -15,6 +15,7 @@ steps: env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Electron and Playwright + retryCountOnTaskFailure: 3 - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index d733ec41efd45..bd0334b07ef13 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -136,7 +136,7 @@ steps: - template: ../common/install-builtin-extensions.yml - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - powershell: node build\lib\policies displayName: Generate Group Policy definitions retryCountOnTaskFailure: 3 @@ -148,7 +148,7 @@ steps: displayName: Transpile - ${{ else }}: - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), eq(parameters.VSCODE_QUALITY, 'insider')) }}: - powershell: node build/win32/explorer-appx-fetcher .build/win32/appx displayName: Download Explorer Sparse Package diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index beb2b9730b077..d5265cab426d0 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,27 +1,27 @@ -3ba067c6f338f9a525c4b697e9cf8e3c3b3d9f6abfdfb11fba47e053da0f3496 *electron-v22.3.14-darwin-arm64-symbols.zip -c08bf19e11c006346b210585cf0803cd0b07107a362a2414cc185f6a228afbf2 *electron-v22.3.14-darwin-arm64.zip -72ced94e7230d3138dd84acbf38dc593d4a93ec796a3a478f99aa6974030d79c *electron-v22.3.14-darwin-x64-symbols.zip -77c1c96411326b00d3ef7c9f6af96a3b4c2fa2314196fce3374fcf734dd8dc67 *electron-v22.3.14-darwin-x64.zip -d847c59f3835749dcdd5376daefb3a5992f1ed5d7693f871328296e1388fb69d *electron-v22.3.14-linux-arm64-symbols.zip -95bb9ee160c60b50ff25b307fb8bc36bdb5297d43c6e366f0b835f36c4f327c9 *electron-v22.3.14-linux-arm64.zip -38a51d81f9ffe6e2ebf25844999fddbeb4edc63ae22136af1502db373bb024ab *electron-v22.3.14-linux-armv7l-symbols.zip -bf589c74f07fe11586ffcf8c122d34b91c5ced08d54532ee883d1e025b6d1b02 *electron-v22.3.14-linux-armv7l.zip -28d0eda61ea736375c549d0955f36b7d3e3c2019453ef83d793dae8b0d74f461 *electron-v22.3.14-linux-x64-symbols.zip -89b72e40fb8b9106deda3e6ffa30dd80beaa8f2e2a9d037b55c034a5a27a7b60 *electron-v22.3.14-linux-x64.zip -b9ba15fcf7c60cf57e95fae731bc0c336e131ed4fac91b4c59d50a28407ca0b0 *electron-v22.3.14-win32-arm64-pdb.zip -9f375d01feeb9e28f9c0913a4e22be900c0a7ff4e51449bb9b859ce1bd18f9f3 *electron-v22.3.14-win32-arm64-symbols.zip -17e354aca0683f79d79f7fa7ecfa8a4381b356d04fa45ec0aa85b5f048151c10 *electron-v22.3.14-win32-arm64.zip -900ca316ce939547ab62847c8833a78c1002a69b936be7e9af328a3518a7d379 *electron-v22.3.14-win32-ia32-pdb.zip -90af7a48b4e722a3436b6a8893540fb746d99b4832ca48c355a63fa0930f6446 *electron-v22.3.14-win32-ia32-symbols.zip -487d811c7cf3282f4c3a17b5ab7ab1fd71dbc585449d77da3a9bf052657ac4ad *electron-v22.3.14-win32-ia32.zip -41ce6c3d87c89f6b48aac74649657a120c28c78513908996dc20e57a640d4653 *electron-v22.3.14-win32-x64-pdb.zip -57b35bfa186b64a9dd1eb2bb85141bb998d0378bb20ac8038718b41d16deb978 *electron-v22.3.14-win32-x64-symbols.zip -f45eba3faa7e10fb1c6e5cf044dd42733a7c8cb455de57647b74e7510b0b94b6 *electron-v22.3.14-win32-x64.zip -16a75de6e3e4643589237e6e1c680c43b4e77fe04918bfbe4408775b7e616afc *ffmpeg-v22.3.14-darwin-arm64.zip -92db0c163c326d33a516ebfc56c7bd4faae9456f4238dde916c580b459b8dc8d *ffmpeg-v22.3.14-darwin-x64.zip -59d2e2b2f2cc515a86a4e0cfd1116d10a8b25a8d58d45bb04de3512e156c944b *ffmpeg-v22.3.14-linux-arm64.zip -b9d3b227bee17666d395ee7882ef477a733c3eeef3f1d9f2e3616d2d02eb3376 *ffmpeg-v22.3.14-linux-armv7l.zip -fa07ef910b23a4ef4b6761bc16d20c0e70ff0259325c4d523129e2d9c5084174 *ffmpeg-v22.3.14-linux-x64.zip -7f744b657ae7c26f80cae0f2771a00edd368350229b85118a246573987dd6ff1 *ffmpeg-v22.3.14-win32-arm64.zip -562e04d2cf1c970b6128d66d08dfe8d88a28e54adf599293eee2bd6c292fd16b *ffmpeg-v22.3.14-win32-ia32.zip -f69510384ef912fd9b4961f97357789a4a36e8df6ff382aeeab23fbb063def9a *ffmpeg-v22.3.14-win32-x64.zip +9ba54e68520fe94b15ab6224333f5e63538f78aa0356545130ac06e308b54a72 *electron-v22.3.17-darwin-arm64-symbols.zip +37aa86e637c1306aa0e7b382d3d3d23717ac4769114f00f5abd98a7b95a97a00 *electron-v22.3.17-darwin-arm64.zip +bfd7ffb2a4b2fa8f47a72ea35340c51b21bc3f0f6e85679e48eb30615110e7dd *electron-v22.3.17-darwin-x64-symbols.zip +87e063025bfd11b60cbb637ecf077dc52d53ba5ac76b828ab06a8c5a66b0b590 *electron-v22.3.17-darwin-x64.zip +1e885e3e10fc952e7c898b91fe0b327147d5ba2effa291a8dff63b84ed6f7f09 *electron-v22.3.17-linux-arm64-symbols.zip +d810449d93ddbe7ec81b3792dc4c7237c7ad52e03d15503316029afe95281ef6 *electron-v22.3.17-linux-arm64.zip +b1112192a53388754fc55018f1f5f116cd342d78cc2beedcd115f313c669975e *electron-v22.3.17-linux-armv7l-symbols.zip +647f375119b8611d9a6ec6b4837c246f2e02d6b5ac75eb21a7a0e8bd07717cc6 *electron-v22.3.17-linux-armv7l.zip +8d4c2743c8a8b4b48364bd27e05eb4e1d22b46ed93c978646fad37def2047722 *electron-v22.3.17-linux-x64-symbols.zip +030c540a88998112f8848f2ecc1429790bb90a19f1826eda0b4e89d2b6e2459c *electron-v22.3.17-linux-x64.zip +afa64d9f5523564d48a77c5e53f9ffdf9b9f144bd4ccf55577a8aa1532e7f5d8 *electron-v22.3.17-win32-arm64-pdb.zip +fd8dc02edf2b7120d9b79e06701001d192dc0007503961769bda3e680ad9cbc2 *electron-v22.3.17-win32-arm64-symbols.zip +80dc7fd40f47832757fb150b63cf3a6d14aad3d64b4891a7a3e2785bd4c98f2d *electron-v22.3.17-win32-arm64.zip +e30077316165162d8954e7af1bb0e3454587efb82c26a65269b0e5a0237a739e *electron-v22.3.17-win32-ia32-pdb.zip +feb05e2ee1555de1721b93613db52a4b42f3caa287b28b5e0918dee7d1d321ca *electron-v22.3.17-win32-ia32-symbols.zip +221e988045f9fd299bb00b27cdde31a7a6621a40cea3ad34efe584e988046766 *electron-v22.3.17-win32-ia32.zip +295c820c5f47ad02bdd5f23d5071ff551e14aa3b018a4daff4cb9b18694862a1 *electron-v22.3.17-win32-x64-pdb.zip +4fe6e8f71fe7ade774ff92a5f3dbb3d667185da3c4be10cf6ade5e2700e32cc6 *electron-v22.3.17-win32-x64-symbols.zip +9a3847ff2a2702a62b66a309368f7feced29294168155de6271d1409191441bf *electron-v22.3.17-win32-x64.zip +b1d258f2378b326d52ba1b4dbd55d49b8190e23ef00d3ccdec9407114242a8d8 *ffmpeg-v22.3.17-darwin-arm64.zip +7ad943f4bacff4379b751fd64db85adb68c2a0456c0fe4fe3d0eb2c50257bccf *ffmpeg-v22.3.17-darwin-x64.zip +59d2e2b2f2cc515a86a4e0cfd1116d10a8b25a8d58d45bb04de3512e156c944b *ffmpeg-v22.3.17-linux-arm64.zip +b9d3b227bee17666d395ee7882ef477a733c3eeef3f1d9f2e3616d2d02eb3376 *ffmpeg-v22.3.17-linux-armv7l.zip +fa07ef910b23a4ef4b6761bc16d20c0e70ff0259325c4d523129e2d9c5084174 *ffmpeg-v22.3.17-linux-x64.zip +a83e1314a9161ebf7ffae9c67bb9332f231f3b82e6ffb68de4704dd81489301c *ffmpeg-v22.3.17-win32-arm64.zip +725b025bbe8664733c5e5d6ee5d2baa36acabd415e0fbd21b056de968a1fe2cd *ffmpeg-v22.3.17-win32-ia32.zip +e4c76b9bb3826e8b56c720220cff0bd1fa1a34e69a26617cd3650bda452bff71 *ffmpeg-v22.3.17-win32-x64.zip diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 90f75ccfabd7f..0d7d3c5b7f80c 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -297,12 +297,14 @@ const BUILD_TARGETS = [ BUILD_TARGETS.forEach(({ arch }) => { const debArch = getDebPackageArch(arch); const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(util.rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); - const buildDebTask = task.define(`vscode-linux-${arch}-build-deb`, task.series(prepareDebTask, buildDebPackage(arch))); + gulp.task(prepareDebTask); + const buildDebTask = task.define(`vscode-linux-${arch}-build-deb`, buildDebPackage(arch)); gulp.task(buildDebTask); const rpmArch = getRpmPackageArch(arch); const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(util.rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); - const buildRpmTask = task.define(`vscode-linux-${arch}-build-rpm`, task.series(prepareRpmTask, buildRpmPackage(arch))); + gulp.task(prepareRpmTask); + const buildRpmTask = task.define(`vscode-linux-${arch}-build-rpm`, buildRpmPackage(arch)); gulp.task(buildRpmTask); const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(util.rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index bfcddd7d1b006..8929f43ee4a65 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -697,6 +697,7 @@ "--background-light", "--dropdown-padding-bottom", "--dropdown-padding-top", + "--hover-maxWidth", "--insert-border-color", "--last-tab-margin-right", "--monaco-monospace-font", @@ -755,6 +756,7 @@ "--z-index-notebook-progress-bar", "--z-index-notebook-scrollbar", "--z-index-run-button-container", + "--z-index-notebook-sticky-scroll", "--zoom-factor" ] } diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index 2444e401703de..0a818aded80c3 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -38,8 +38,10 @@ exports.referenceGeneratedDepsByArch = { 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.16.0)', 'libglib2.0-0 (>= 2.39.4)', + 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.22)', 'libnss3 (>= 3.26)', @@ -76,8 +78,10 @@ exports.referenceGeneratedDepsByArch = { 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.12.0)', 'libglib2.0-0 (>= 2.39.4)', + 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.22)', 'libnss3 (>= 3.26)', @@ -113,8 +117,10 @@ exports.referenceGeneratedDepsByArch = { 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.12.0)', 'libglib2.0-0 (>= 2.39.4)', + 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.22)', 'libnss3 (>= 3.26)', @@ -136,4 +142,4 @@ exports.referenceGeneratedDepsByArch = { 'xdg-utils (>= 1.0.2)' ] }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLGtIQUFrSDtBQUNsSCw0REFBNEQ7QUFDL0MsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCO0lBQ2pCLHFDQUFxQztJQUNyQyxtQkFBbUI7SUFDbkIsc0RBQXNEO0lBQ3RELHNCQUFzQixDQUFDLGlCQUFpQjtDQUN4QyxDQUFDO0FBRUYsb0hBQW9IO0FBQ3BILDBDQUEwQztBQUMxQyw4REFBOEQ7QUFDakQsUUFBQSxlQUFlLEdBQUc7SUFDOUIsWUFBWSxDQUFDLHlFQUF5RTtDQUN0RixDQUFDO0FBRVcsUUFBQSw0QkFBNEIsR0FBRztJQUMzQyxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixpQkFBaUI7UUFDakIsa0JBQWtCO1FBQ2xCLHNCQUFzQjtRQUN0QixzREFBc0Q7UUFDdEQseUJBQXlCO1FBQ3pCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIseUJBQXlCO1FBQ3pCLDBCQUEwQjtRQUMxQiwwQkFBMEI7UUFDMUIsd0JBQXdCO1FBQ3hCLHFDQUFxQztRQUNyQyx3QkFBd0I7UUFDeEIscUJBQXFCO1FBQ3JCLG1CQUFtQjtRQUNuQiw0QkFBNEI7UUFDNUIseUJBQXlCO1FBQ3pCLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7SUFDRCxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixpQkFBaUI7UUFDakIsZ0JBQWdCO1FBQ2hCLGdCQUFnQjtRQUNoQixnQkFBZ0I7UUFDaEIsc0JBQXNCO1FBQ3RCLHNEQUFzRDtRQUN0RCx5QkFBeUI7UUFDekIscUJBQXFCO1FBQ3JCLHNCQUFzQjtRQUN0Qix5QkFBeUI7UUFDekIsMEJBQTBCO1FBQzFCLDBCQUEwQjtRQUMxQix3QkFBd0I7UUFDeEIscUNBQXFDO1FBQ3JDLHdCQUF3QjtRQUN4QixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLDRCQUE0QjtRQUM1Qix5QkFBeUI7UUFDekIsbUJBQW1CO1FBQ25CLHFCQUFxQjtRQUNyQixtQkFBbUI7UUFDbkIsVUFBVTtRQUNWLDBCQUEwQjtRQUMxQixvQkFBb0I7UUFDcEIsK0JBQStCO1FBQy9CLHdCQUF3QjtRQUN4QixVQUFVO1FBQ1YsWUFBWTtRQUNaLDBCQUEwQjtRQUMxQixhQUFhO1FBQ2IsWUFBWTtRQUNaLHNCQUFzQjtLQUN0QjtJQUNELE9BQU8sRUFBRTtRQUNSLGlCQUFpQjtRQUNqQix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLHdCQUF3QjtRQUN4QiwyQkFBMkI7UUFDM0IsaUJBQWlCO1FBQ2pCLHNCQUFzQjtRQUN0QixzREFBc0Q7UUFDdEQsd0JBQXdCO1FBQ3hCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIseUJBQXlCO1FBQ3pCLDBCQUEwQjtRQUMxQiwwQkFBMEI7UUFDMUIsd0JBQXdCO1FBQ3hCLHFDQUFxQztRQUNyQyx3QkFBd0I7UUFDeEIscUJBQXFCO1FBQ3JCLG1CQUFtQjtRQUNuQiw0QkFBNEI7UUFDNUIseUJBQXlCO1FBQ3pCLG1CQUFtQjtRQUNuQixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7Q0FDRCxDQUFDIn0= \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLGtIQUFrSDtBQUNsSCw0REFBNEQ7QUFDL0MsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCO0lBQ2pCLHFDQUFxQztJQUNyQyxtQkFBbUI7SUFDbkIsc0RBQXNEO0lBQ3RELHNCQUFzQixDQUFDLGlCQUFpQjtDQUN4QyxDQUFDO0FBRUYsb0hBQW9IO0FBQ3BILDBDQUEwQztBQUMxQyw4REFBOEQ7QUFDakQsUUFBQSxlQUFlLEdBQUc7SUFDOUIsWUFBWSxDQUFDLHlFQUF5RTtDQUN0RixDQUFDO0FBRVcsUUFBQSw0QkFBNEIsR0FBRztJQUMzQyxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixpQkFBaUI7UUFDakIsa0JBQWtCO1FBQ2xCLHNCQUFzQjtRQUN0QixzREFBc0Q7UUFDdEQseUJBQXlCO1FBQ3pCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIseUJBQXlCO1FBQ3pCLDBCQUEwQjtRQUMxQiwwQkFBMEI7UUFDMUIsNEJBQTRCO1FBQzVCLHdCQUF3QjtRQUN4QixxQ0FBcUM7UUFDckMsMkJBQTJCO1FBQzNCLHdCQUF3QjtRQUN4QixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLDRCQUE0QjtRQUM1Qix5QkFBeUI7UUFDekIsVUFBVTtRQUNWLDBCQUEwQjtRQUMxQixvQkFBb0I7UUFDcEIsK0JBQStCO1FBQy9CLHdCQUF3QjtRQUN4QixVQUFVO1FBQ1YsWUFBWTtRQUNaLDBCQUEwQjtRQUMxQixhQUFhO1FBQ2IsWUFBWTtRQUNaLHNCQUFzQjtLQUN0QjtJQUNELE9BQU8sRUFBRTtRQUNSLGlCQUFpQjtRQUNqQix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLHdCQUF3QjtRQUN4QiwyQkFBMkI7UUFDM0IsaUJBQWlCO1FBQ2pCLGlCQUFpQjtRQUNqQixnQkFBZ0I7UUFDaEIsZ0JBQWdCO1FBQ2hCLGdCQUFnQjtRQUNoQixzQkFBc0I7UUFDdEIsc0RBQXNEO1FBQ3RELHlCQUF5QjtRQUN6QixxQkFBcUI7UUFDckIsc0JBQXNCO1FBQ3RCLHlCQUF5QjtRQUN6QiwwQkFBMEI7UUFDMUIsMEJBQTBCO1FBQzFCLDRCQUE0QjtRQUM1Qix3QkFBd0I7UUFDeEIscUNBQXFDO1FBQ3JDLDJCQUEyQjtRQUMzQix3QkFBd0I7UUFDeEIscUJBQXFCO1FBQ3JCLG1CQUFtQjtRQUNuQiw0QkFBNEI7UUFDNUIseUJBQXlCO1FBQ3pCLG1CQUFtQjtRQUNuQixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7SUFDRCxPQUFPLEVBQUU7UUFDUixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsMkJBQTJCO1FBQzNCLGlCQUFpQjtRQUNqQixzQkFBc0I7UUFDdEIsc0RBQXNEO1FBQ3RELHdCQUF3QjtRQUN4QixxQkFBcUI7UUFDckIsc0JBQXNCO1FBQ3RCLHlCQUF5QjtRQUN6QiwwQkFBMEI7UUFDMUIsMEJBQTBCO1FBQzFCLDRCQUE0QjtRQUM1Qix3QkFBd0I7UUFDeEIscUNBQXFDO1FBQ3JDLDJCQUEyQjtRQUMzQix3QkFBd0I7UUFDeEIscUJBQXFCO1FBQ3JCLG1CQUFtQjtRQUNuQiw0QkFBNEI7UUFDNUIseUJBQXlCO1FBQ3pCLG1CQUFtQjtRQUNuQixxQkFBcUI7UUFDckIsbUJBQW1CO1FBQ25CLFVBQVU7UUFDViwwQkFBMEI7UUFDMUIsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQix3QkFBd0I7UUFDeEIsVUFBVTtRQUNWLFlBQVk7UUFDWiwwQkFBMEI7UUFDMUIsYUFBYTtRQUNiLFlBQVk7UUFDWixzQkFBc0I7S0FDdEI7Q0FDRCxDQUFDIn0= \ No newline at end of file diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 7f6cd6ca8ccfb..72af03d75bd42 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -38,8 +38,10 @@ export const referenceGeneratedDepsByArch = { 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.16.0)', 'libglib2.0-0 (>= 2.39.4)', + 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.22)', 'libnss3 (>= 3.26)', @@ -76,8 +78,10 @@ export const referenceGeneratedDepsByArch = { 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.12.0)', 'libglib2.0-0 (>= 2.39.4)', + 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.22)', 'libnss3 (>= 3.26)', @@ -113,8 +117,10 @@ export const referenceGeneratedDepsByArch = { 'libgbm1 (>= 17.1.0~rc2)', 'libglib2.0-0 (>= 2.12.0)', 'libglib2.0-0 (>= 2.39.4)', + 'libgssapi-krb5-2 (>= 1.17)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libkrb5-3 (>= 1.6.dfsg.2)', 'libnspr4 (>= 2:4.9-2~)', 'libnss3 (>= 2:3.22)', 'libnss3 (>= 3.26)', diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index c836348ef5105..bfbcb6763d50c 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -65,7 +65,11 @@ exports.referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', + 'libgssapi_krb5.so.2()(64bit)', + 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', + 'libkrb5.so.3()(64bit)', + 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.2.5)(64bit)', 'libnspr4.so()(64bit)', @@ -146,8 +150,12 @@ exports.referenceGeneratedDepsByArch = { 'libgio-2.0.so.0', 'libglib-2.0.so.0', 'libgobject-2.0.so.0', + 'libgssapi_krb5.so.2', + 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)', 'libgtk-3.so.0', 'libgtk-3.so.0()(64bit)', + 'libkrb5.so.3', + 'libkrb5.so.3(krb5_3_MIT)', 'libm.so.6', 'libm.so.6(GLIBC_2.4)', 'libnspr4.so', @@ -236,7 +244,11 @@ exports.referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', + 'libgssapi_krb5.so.2()(64bit)', + 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', + 'libkrb5.so.3()(64bit)', + 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.17)(64bit)', 'libnspr4.so()(64bit)', @@ -289,4 +301,4 @@ exports.referenceGeneratedDepsByArch = { 'xdg-utils' ] }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLCtHQUErRztBQUMvRywrREFBK0Q7QUFDbEQsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCO0lBQ2pCLHdCQUF3QjtJQUN4Qiw2QkFBNkI7SUFDN0IsNkJBQTZCO0lBQzdCLGdDQUFnQztJQUNoQyx5QkFBeUI7SUFDekIsdUJBQXVCO0lBQ3ZCLFdBQVcsQ0FBQyxpQkFBaUI7Q0FDN0IsQ0FBQztBQUVXLFFBQUEsNEJBQTRCLEdBQUc7SUFDM0MsUUFBUSxFQUFFO1FBQ1QsaUJBQWlCO1FBQ2pCLCtCQUErQjtRQUMvQiwwQ0FBMEM7UUFDMUMsd0NBQXdDO1FBQ3hDLHNCQUFzQjtRQUN0Qiw2QkFBNkI7UUFDN0IsMEJBQTBCO1FBQzFCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIseUJBQXlCO1FBQ3pCLHlCQUF5QjtRQUN6QixpQ0FBaUM7UUFDakMsc0NBQXNDO1FBQ3RDLDBCQUEwQjtRQUMxQixpQ0FBaUM7UUFDakMsd0JBQXdCO1FBQ3hCLG9CQUFvQjtRQUNwQiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5Qiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QiwrQkFBK0I7UUFDL0IsNkJBQTZCO1FBQzdCLCtCQUErQjtRQUMvQiwrQkFBK0I7UUFDL0IsK0JBQStCO1FBQy9CLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0IsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0Isd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIscUJBQXFCO1FBQ3JCLGdDQUFnQztRQUNoQyxzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLDBCQUEwQjtRQUMxQiwyQkFBMkI7UUFDM0IsOEJBQThCO1FBQzlCLHdCQUF3QjtRQUN4QixvQkFBb0I7UUFDcEIsK0JBQStCO1FBQy9CLHNCQUFzQjtRQUN0QixxQkFBcUI7UUFDckIsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3QiwrQkFBK0I7UUFDL0IsNEJBQTRCO1FBQzVCLDZCQUE2QjtRQUM3Qiw0QkFBNEI7UUFDNUIsNEJBQTRCO1FBQzVCLDRCQUE0QjtRQUM1Qiw4QkFBOEI7UUFDOUIseUJBQXlCO1FBQ3pCLHVDQUF1QztRQUN2Qyw0QkFBNEI7UUFDNUIsMEJBQTBCO1FBQzFCLG9DQUFvQztRQUNwQyxxQ0FBcUM7UUFDckMscUNBQXFDO1FBQ3JDLHFDQUFxQztRQUNyQyxxQ0FBcUM7UUFDckMscUJBQXFCO1FBQ3JCLGdDQUFnQztRQUNoQywyQkFBMkI7UUFDM0IsdUJBQXVCO1FBQ3ZCLCtCQUErQjtRQUMvQiw4QkFBOEI7UUFDOUIsNkJBQTZCO1FBQzdCLHVCQUF1QjtRQUN2QixrQ0FBa0M7UUFDbEMsc0JBQXNCO1FBQ3RCLDRCQUE0QjtRQUM1QiwwQkFBMEI7UUFDMUIsZ0NBQWdDO1FBQ2hDLGdCQUFnQjtRQUNoQixXQUFXO0tBQ1g7SUFDRCxTQUFTLEVBQUU7UUFDVixpQkFBaUI7UUFDakIscUJBQXFCO1FBQ3JCLGdDQUFnQztRQUNoQyxhQUFhO1FBQ2Isb0JBQW9CO1FBQ3BCLGlCQUFpQjtRQUNqQixjQUFjO1FBQ2QsZ0JBQWdCO1FBQ2hCLGdCQUFnQjtRQUNoQixnQkFBZ0I7UUFDaEIsMEJBQTBCO1FBQzFCLCtCQUErQjtRQUMvQixpQkFBaUI7UUFDakIsd0JBQXdCO1FBQ3hCLGVBQWU7UUFDZixXQUFXO1FBQ1gsdUJBQXVCO1FBQ3ZCLHVCQUF1QjtRQUN2Qix1QkFBdUI7UUFDdkIsdUJBQXVCO1FBQ3ZCLHVCQUF1QjtRQUN2Qix1QkFBdUI7UUFDdkIsc0JBQXNCO1FBQ3RCLHNCQUFzQjtRQUN0QixzQkFBc0I7UUFDdEIsc0JBQXNCO1FBQ3RCLHNCQUFzQjtRQUN0QixlQUFlO1FBQ2YsdUJBQXVCO1FBQ3ZCLGdCQUFnQjtRQUNoQixZQUFZO1FBQ1osdUJBQXVCO1FBQ3ZCLGFBQWE7UUFDYixlQUFlO1FBQ2YsYUFBYTtRQUNiLGVBQWU7UUFDZix3QkFBd0I7UUFDeEIsd0JBQXdCO1FBQ3hCLGlCQUFpQjtRQUNqQixrQkFBa0I7UUFDbEIscUJBQXFCO1FBQ3JCLGVBQWU7UUFDZix3QkFBd0I7UUFDeEIsV0FBVztRQUNYLHNCQUFzQjtRQUN0QixhQUFhO1FBQ2IsWUFBWTtRQUNaLHNCQUFzQjtRQUN0QixzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLHFCQUFxQjtRQUNyQixzQkFBc0I7UUFDdEIsNkJBQTZCO1FBQzdCLHFCQUFxQjtRQUNyQixxQkFBcUI7UUFDckIscUJBQXFCO1FBQ3JCLHVCQUF1QjtRQUN2QixnQkFBZ0I7UUFDaEIsZ0NBQWdDO1FBQ2hDLG1CQUFtQjtRQUNuQixpQkFBaUI7UUFDakIsNkJBQTZCO1FBQzdCLDRCQUE0QjtRQUM1QixZQUFZO1FBQ1osdUJBQXVCO1FBQ3ZCLGtCQUFrQjtRQUNsQixjQUFjO1FBQ2Qsd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2Qiw2QkFBNkI7UUFDN0IsZ0JBQWdCO1FBQ2hCLDRCQUE0QjtRQUM1Qiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QixrQ0FBa0M7UUFDbEMsNkJBQTZCO1FBQzdCLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsZ0NBQWdDO1FBQ2hDLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsZ0NBQWdDO1FBQ2hDLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsK0JBQStCO1FBQy9CLCtCQUErQjtRQUMvQixjQUFjO1FBQ2QseUJBQXlCO1FBQ3pCLGFBQWE7UUFDYixtQkFBbUI7UUFDbkIsaUJBQWlCO1FBQ2pCLGdDQUFnQztRQUNoQyxnQkFBZ0I7UUFDaEIsV0FBVztLQUNYO0lBQ0QsU0FBUyxFQUFFO1FBQ1YsaUJBQWlCO1FBQ2pCLGdDQUFnQztRQUNoQywwQ0FBMEM7UUFDMUMsc0JBQXNCO1FBQ3RCLDZCQUE2QjtRQUM3QiwwQkFBMEI7UUFDMUIsdUJBQXVCO1FBQ3ZCLHlCQUF5QjtRQUN6Qix5QkFBeUI7UUFDekIseUJBQXlCO1FBQ3pCLGlDQUFpQztRQUNqQyxzQ0FBc0M7UUFDdEMsMEJBQTBCO1FBQzFCLGlDQUFpQztRQUNqQyx3QkFBd0I7UUFDeEIsb0JBQW9CO1FBQ3BCLDhCQUE4QjtRQUM5Qiw4QkFBOEI7UUFDOUIsd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIsb0NBQW9DO1FBQ3BDLHFCQUFxQjtRQUNyQiwrQkFBK0I7UUFDL0Isc0JBQXNCO1FBQ3RCLHdCQUF3QjtRQUN4QixzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLCtCQUErQjtRQUMvQixpQ0FBaUM7UUFDakMsaUNBQWlDO1FBQ2pDLDBCQUEwQjtRQUMxQiwyQkFBMkI7UUFDM0IsOEJBQThCO1FBQzlCLHdCQUF3QjtRQUN4QixvQkFBb0I7UUFDcEIsOEJBQThCO1FBQzlCLHNCQUFzQjtRQUN0QixxQkFBcUI7UUFDckIsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3QiwrQkFBK0I7UUFDL0IsNEJBQTRCO1FBQzVCLDZCQUE2QjtRQUM3Qiw0QkFBNEI7UUFDNUIsNEJBQTRCO1FBQzVCLDRCQUE0QjtRQUM1Qiw4QkFBOEI7UUFDOUIseUJBQXlCO1FBQ3pCLHVDQUF1QztRQUN2Qyw0QkFBNEI7UUFDNUIsMEJBQTBCO1FBQzFCLG9DQUFvQztRQUNwQyxxQkFBcUI7UUFDckIsK0JBQStCO1FBQy9CLDJCQUEyQjtRQUMzQix1QkFBdUI7UUFDdkIsK0JBQStCO1FBQy9CLDhCQUE4QjtRQUM5Qiw2QkFBNkI7UUFDN0IseUJBQXlCO1FBQ3pCLG1DQUFtQztRQUNuQyxxQ0FBcUM7UUFDckMscUNBQXFDO1FBQ3JDLHFDQUFxQztRQUNyQyxvQ0FBb0M7UUFDcEMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2Qyx1Q0FBdUM7UUFDdkMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2Qyx1Q0FBdUM7UUFDdkMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2QyxzQ0FBc0M7UUFDdEMsc0NBQXNDO1FBQ3RDLHVCQUF1QjtRQUN2QixpQ0FBaUM7UUFDakMsc0JBQXNCO1FBQ3RCLDRCQUE0QjtRQUM1QixtQ0FBbUM7UUFDbkMsMEJBQTBCO1FBQzFCLGdDQUFnQztRQUNoQyxnQkFBZ0I7UUFDaEIsV0FBVztLQUNYO0NBQ0QsQ0FBQyJ9 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwLWxpc3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwLWxpc3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7O2dHQUdnRzs7O0FBRWhHLCtHQUErRztBQUMvRywrREFBK0Q7QUFDbEQsUUFBQSxjQUFjLEdBQUc7SUFDN0IsaUJBQWlCO0lBQ2pCLHdCQUF3QjtJQUN4Qiw2QkFBNkI7SUFDN0IsNkJBQTZCO0lBQzdCLGdDQUFnQztJQUNoQyx5QkFBeUI7SUFDekIsdUJBQXVCO0lBQ3ZCLFdBQVcsQ0FBQyxpQkFBaUI7Q0FDN0IsQ0FBQztBQUVXLFFBQUEsNEJBQTRCLEdBQUc7SUFDM0MsUUFBUSxFQUFFO1FBQ1QsaUJBQWlCO1FBQ2pCLCtCQUErQjtRQUMvQiwwQ0FBMEM7UUFDMUMsd0NBQXdDO1FBQ3hDLHNCQUFzQjtRQUN0Qiw2QkFBNkI7UUFDN0IsMEJBQTBCO1FBQzFCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIseUJBQXlCO1FBQ3pCLHlCQUF5QjtRQUN6QixpQ0FBaUM7UUFDakMsc0NBQXNDO1FBQ3RDLDBCQUEwQjtRQUMxQixpQ0FBaUM7UUFDakMsd0JBQXdCO1FBQ3hCLG9CQUFvQjtRQUNwQiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5Qiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QiwrQkFBK0I7UUFDL0IsNkJBQTZCO1FBQzdCLCtCQUErQjtRQUMvQiwrQkFBK0I7UUFDL0IsK0JBQStCO1FBQy9CLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0IsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0Isd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2Qix5QkFBeUI7UUFDekIscUJBQXFCO1FBQ3JCLGdDQUFnQztRQUNoQyxzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLDBCQUEwQjtRQUMxQiwyQkFBMkI7UUFDM0IsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5QiwrQ0FBK0M7UUFDL0Msd0JBQXdCO1FBQ3hCLHVCQUF1QjtRQUN2QixpQ0FBaUM7UUFDakMsb0JBQW9CO1FBQ3BCLCtCQUErQjtRQUMvQixzQkFBc0I7UUFDdEIscUJBQXFCO1FBQ3JCLDZCQUE2QjtRQUM3Qiw2QkFBNkI7UUFDN0IsK0JBQStCO1FBQy9CLDRCQUE0QjtRQUM1Qiw2QkFBNkI7UUFDN0IsNEJBQTRCO1FBQzVCLDRCQUE0QjtRQUM1Qiw0QkFBNEI7UUFDNUIsOEJBQThCO1FBQzlCLHlCQUF5QjtRQUN6Qix1Q0FBdUM7UUFDdkMsNEJBQTRCO1FBQzVCLDBCQUEwQjtRQUMxQixvQ0FBb0M7UUFDcEMscUNBQXFDO1FBQ3JDLHFDQUFxQztRQUNyQyxxQ0FBcUM7UUFDckMscUNBQXFDO1FBQ3JDLHFCQUFxQjtRQUNyQixnQ0FBZ0M7UUFDaEMsMkJBQTJCO1FBQzNCLHVCQUF1QjtRQUN2QiwrQkFBK0I7UUFDL0IsOEJBQThCO1FBQzlCLDZCQUE2QjtRQUM3Qix1QkFBdUI7UUFDdkIsa0NBQWtDO1FBQ2xDLHNCQUFzQjtRQUN0Qiw0QkFBNEI7UUFDNUIsMEJBQTBCO1FBQzFCLGdDQUFnQztRQUNoQyxnQkFBZ0I7UUFDaEIsV0FBVztLQUNYO0lBQ0QsU0FBUyxFQUFFO1FBQ1YsaUJBQWlCO1FBQ2pCLHFCQUFxQjtRQUNyQixnQ0FBZ0M7UUFDaEMsYUFBYTtRQUNiLG9CQUFvQjtRQUNwQixpQkFBaUI7UUFDakIsY0FBYztRQUNkLGdCQUFnQjtRQUNoQixnQkFBZ0I7UUFDaEIsZ0JBQWdCO1FBQ2hCLDBCQUEwQjtRQUMxQiwrQkFBK0I7UUFDL0IsaUJBQWlCO1FBQ2pCLHdCQUF3QjtRQUN4QixlQUFlO1FBQ2YsV0FBVztRQUNYLHVCQUF1QjtRQUN2Qix1QkFBdUI7UUFDdkIsdUJBQXVCO1FBQ3ZCLHVCQUF1QjtRQUN2Qix1QkFBdUI7UUFDdkIsdUJBQXVCO1FBQ3ZCLHNCQUFzQjtRQUN0QixzQkFBc0I7UUFDdEIsc0JBQXNCO1FBQ3RCLHNCQUFzQjtRQUN0QixzQkFBc0I7UUFDdEIsZUFBZTtRQUNmLHVCQUF1QjtRQUN2QixnQkFBZ0I7UUFDaEIsWUFBWTtRQUNaLHVCQUF1QjtRQUN2QixhQUFhO1FBQ2IsZUFBZTtRQUNmLGFBQWE7UUFDYixlQUFlO1FBQ2Ysd0JBQXdCO1FBQ3hCLHdCQUF3QjtRQUN4QixpQkFBaUI7UUFDakIsa0JBQWtCO1FBQ2xCLHFCQUFxQjtRQUNyQixxQkFBcUI7UUFDckIsd0NBQXdDO1FBQ3hDLGVBQWU7UUFDZix3QkFBd0I7UUFDeEIsY0FBYztRQUNkLDBCQUEwQjtRQUMxQixXQUFXO1FBQ1gsc0JBQXNCO1FBQ3RCLGFBQWE7UUFDYixZQUFZO1FBQ1osc0JBQXNCO1FBQ3RCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIscUJBQXFCO1FBQ3JCLHNCQUFzQjtRQUN0Qiw2QkFBNkI7UUFDN0IscUJBQXFCO1FBQ3JCLHFCQUFxQjtRQUNyQixxQkFBcUI7UUFDckIsdUJBQXVCO1FBQ3ZCLGdCQUFnQjtRQUNoQixnQ0FBZ0M7UUFDaEMsbUJBQW1CO1FBQ25CLGlCQUFpQjtRQUNqQiw2QkFBNkI7UUFDN0IsNEJBQTRCO1FBQzVCLFlBQVk7UUFDWix1QkFBdUI7UUFDdkIsa0JBQWtCO1FBQ2xCLGNBQWM7UUFDZCx3QkFBd0I7UUFDeEIsdUJBQXVCO1FBQ3ZCLDZCQUE2QjtRQUM3QixnQkFBZ0I7UUFDaEIsNEJBQTRCO1FBQzVCLDhCQUE4QjtRQUM5Qiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLGtDQUFrQztRQUNsQyw2QkFBNkI7UUFDN0IsZ0NBQWdDO1FBQ2hDLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsZ0NBQWdDO1FBQ2hDLGdDQUFnQztRQUNoQyxnQ0FBZ0M7UUFDaEMsZ0NBQWdDO1FBQ2hDLGdDQUFnQztRQUNoQywrQkFBK0I7UUFDL0IsK0JBQStCO1FBQy9CLGNBQWM7UUFDZCx5QkFBeUI7UUFDekIsYUFBYTtRQUNiLG1CQUFtQjtRQUNuQixpQkFBaUI7UUFDakIsZ0NBQWdDO1FBQ2hDLGdCQUFnQjtRQUNoQixXQUFXO0tBQ1g7SUFDRCxTQUFTLEVBQUU7UUFDVixpQkFBaUI7UUFDakIsZ0NBQWdDO1FBQ2hDLDBDQUEwQztRQUMxQyxzQkFBc0I7UUFDdEIsNkJBQTZCO1FBQzdCLDBCQUEwQjtRQUMxQix1QkFBdUI7UUFDdkIseUJBQXlCO1FBQ3pCLHlCQUF5QjtRQUN6Qix5QkFBeUI7UUFDekIsaUNBQWlDO1FBQ2pDLHNDQUFzQztRQUN0QywwQkFBMEI7UUFDMUIsaUNBQWlDO1FBQ2pDLHdCQUF3QjtRQUN4QixvQkFBb0I7UUFDcEIsOEJBQThCO1FBQzlCLDhCQUE4QjtRQUM5Qix3QkFBd0I7UUFDeEIsdUJBQXVCO1FBQ3ZCLHlCQUF5QjtRQUN6QixvQ0FBb0M7UUFDcEMscUJBQXFCO1FBQ3JCLCtCQUErQjtRQUMvQixzQkFBc0I7UUFDdEIsd0JBQXdCO1FBQ3hCLHNCQUFzQjtRQUN0Qix3QkFBd0I7UUFDeEIsK0JBQStCO1FBQy9CLGlDQUFpQztRQUNqQyxpQ0FBaUM7UUFDakMsMEJBQTBCO1FBQzFCLDJCQUEyQjtRQUMzQiw4QkFBOEI7UUFDOUIsOEJBQThCO1FBQzlCLCtDQUErQztRQUMvQyx3QkFBd0I7UUFDeEIsdUJBQXVCO1FBQ3ZCLGlDQUFpQztRQUNqQyxvQkFBb0I7UUFDcEIsOEJBQThCO1FBQzlCLHNCQUFzQjtRQUN0QixxQkFBcUI7UUFDckIsNkJBQTZCO1FBQzdCLDZCQUE2QjtRQUM3QiwrQkFBK0I7UUFDL0IsNEJBQTRCO1FBQzVCLDZCQUE2QjtRQUM3Qiw0QkFBNEI7UUFDNUIsNEJBQTRCO1FBQzVCLDRCQUE0QjtRQUM1Qiw4QkFBOEI7UUFDOUIseUJBQXlCO1FBQ3pCLHVDQUF1QztRQUN2Qyw0QkFBNEI7UUFDNUIsMEJBQTBCO1FBQzFCLG9DQUFvQztRQUNwQyxxQkFBcUI7UUFDckIsK0JBQStCO1FBQy9CLDJCQUEyQjtRQUMzQix1QkFBdUI7UUFDdkIsK0JBQStCO1FBQy9CLDhCQUE4QjtRQUM5Qiw2QkFBNkI7UUFDN0IseUJBQXlCO1FBQ3pCLG1DQUFtQztRQUNuQyxxQ0FBcUM7UUFDckMscUNBQXFDO1FBQ3JDLHFDQUFxQztRQUNyQyxvQ0FBb0M7UUFDcEMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2Qyx1Q0FBdUM7UUFDdkMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2Qyx1Q0FBdUM7UUFDdkMsdUNBQXVDO1FBQ3ZDLHVDQUF1QztRQUN2QyxzQ0FBc0M7UUFDdEMsc0NBQXNDO1FBQ3RDLHVCQUF1QjtRQUN2QixpQ0FBaUM7UUFDakMsc0JBQXNCO1FBQ3RCLDRCQUE0QjtRQUM1QixtQ0FBbUM7UUFDbkMsMEJBQTBCO1FBQzFCLGdDQUFnQztRQUNoQyxnQkFBZ0I7UUFDaEIsV0FBVztLQUNYO0NBQ0QsQ0FBQyJ9 \ No newline at end of file diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index c262448c318e8..5788ec30f9486 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -64,7 +64,11 @@ export const referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', + 'libgssapi_krb5.so.2()(64bit)', + 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', + 'libkrb5.so.3()(64bit)', + 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.2.5)(64bit)', 'libnspr4.so()(64bit)', @@ -145,8 +149,12 @@ export const referenceGeneratedDepsByArch = { 'libgio-2.0.so.0', 'libglib-2.0.so.0', 'libgobject-2.0.so.0', + 'libgssapi_krb5.so.2', + 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)', 'libgtk-3.so.0', 'libgtk-3.so.0()(64bit)', + 'libkrb5.so.3', + 'libkrb5.so.3(krb5_3_MIT)', 'libm.so.6', 'libm.so.6(GLIBC_2.4)', 'libnspr4.so', @@ -235,7 +243,11 @@ export const referenceGeneratedDepsByArch = { 'libgio-2.0.so.0()(64bit)', 'libglib-2.0.so.0()(64bit)', 'libgobject-2.0.so.0()(64bit)', + 'libgssapi_krb5.so.2()(64bit)', + 'libgssapi_krb5.so.2(gssapi_krb5_2_MIT)(64bit)', 'libgtk-3.so.0()(64bit)', + 'libkrb5.so.3()(64bit)', + 'libkrb5.so.3(krb5_3_MIT)(64bit)', 'libm.so.6()(64bit)', 'libm.so.6(GLIBC_2.17)(64bit)', 'libnspr4.so()(64bit)', diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index d9280ffb1eb87..09df602a3bf5d 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -53,7 +53,10 @@ function yarnInstall(dir, opts) { console.log(`Installing dependencies in ${dir} inside container ${process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME']}...`); opts.cwd = root; - run('docker', ['run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `/mnt/vss/_work/1/s:/root/vscode`, '-v', `/mnt/vss/_work/1/s/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); + if (process.env['npm_config_arch'] === 'arm64') { + run('sudo', ['docker', 'run', '--rm', '--privileged', 'multiarch/qemu-user-static', '--reset', '-p', 'yes'], opts); + } + run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); run('sudo', ['chown', '-R', `${userinfo.uid}:${userinfo.gid}`, `${dir}/node_modules`], opts); } else { console.log(`Installing dependencies in ${dir}...`); diff --git a/build/win32/Cargo.lock b/build/win32/Cargo.lock index f83ae22dfc260..fb5217556906a 100644 --- a/build/win32/Cargo.lock +++ b/build/win32/Cargo.lock @@ -109,7 +109,7 @@ dependencies = [ [[package]] name = "inno_updater" -version = "0.10.0" +version = "0.10.1" dependencies = [ "byteorder", "crc", diff --git a/build/win32/Cargo.toml b/build/win32/Cargo.toml index faf7e7fe6d1a1..cf3cc9de80be5 100644 --- a/build/win32/Cargo.toml +++ b/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.10.0" +version = "0.10.1" authors = ["Microsoft "] build = "build.rs" diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index 941ebfe408145..fa2fd26a466de 100644 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/cgmanifest.json b/cgmanifest.json index d5669cdf185e9..c2812272b5064 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "4ade2a6fb65e4b723feb7c09a5df765e5006b378" + "commitHash": "047fdbf3ca1958e4e489f0e777bf4022540f5ec2" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "22.3.14" + "version": "22.3.17" }, { "component": { diff --git a/cli/build.rs b/cli/build.rs index bcf1bf27e0ac3..41e289774e94f 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -25,7 +25,7 @@ fn apply_build_environment_variables() { } let pkg_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let mut cmd = Command::new("node"); + let mut cmd = Command::new(env::var("NODE_PATH").unwrap_or_else(|_| "node".to_string())); cmd.arg("../build/azure-pipelines/cli/prepare.js"); cmd.current_dir(&pkg_dir); cmd.env("VSCODE_CLI_PREPARE_OUTPUT", "json"); diff --git a/cli/src/async_pipe.rs b/cli/src/async_pipe.rs index dcbe0d1601739..6c7c918967af6 100644 --- a/cli/src/async_pipe.rs +++ b/cli/src/async_pipe.rs @@ -4,7 +4,10 @@ *--------------------------------------------------------------------------------------------*/ use crate::{constants::APPLICATION_NAME, util::errors::CodeError}; +use async_trait::async_trait; use std::path::{Path, PathBuf}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpListener; use uuid::Uuid; // todo: we could probably abstract this into some crate, if one doesn't already exist @@ -39,7 +42,7 @@ cfg_if::cfg_if! { pipe.into_split() } } else { - use tokio::{time::sleep, io::{AsyncRead, AsyncWrite, ReadBuf}}; + use tokio::{time::sleep, io::ReadBuf}; use tokio::net::windows::named_pipe::{ClientOptions, ServerOptions, NamedPipeClient, NamedPipeServer}; use std::{time::Duration, pin::Pin, task::{Context, Poll}, io}; use pin_project::pin_project; @@ -181,3 +184,34 @@ pub fn get_socket_name() -> PathBuf { } } } + +pub type AcceptedRW = ( + Box, + Box, +); + +#[async_trait] +pub trait AsyncRWAccepter { + async fn accept_rw(&mut self) -> Result; +} + +#[async_trait] +impl AsyncRWAccepter for AsyncPipeListener { + async fn accept_rw(&mut self) -> Result { + let pipe = self.accept().await?; + let (read, write) = socket_stream_split(pipe); + Ok((Box::new(read), Box::new(write))) + } +} + +#[async_trait] +impl AsyncRWAccepter for TcpListener { + async fn accept_rw(&mut self) -> Result { + let (stream, _) = self + .accept() + .await + .map_err(CodeError::AsyncPipeListenerFailed)?; + let (read, write) = tokio::io::split(stream); + Ok((Box::new(read), Box::new(write))) + } +} diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index 62e4195c7e4b1..8c32ee14d89f1 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -114,6 +114,9 @@ async fn main() -> Result<(), std::convert::Infallible> { Some(args::TunnelSubcommand::Service(service_args)) => { tunnels::service(context_no_logger(), service_args).await } + Some(args::TunnelSubcommand::ForwardInternal(forward_args)) => { + tunnels::forward(context_no_logger(), forward_args).await + } None => tunnels::serve(context_no_logger(), tunnel_args.serve_args).await, }, }, diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index d34519d6810b0..0051f72f3957d 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -182,6 +182,12 @@ pub struct CommandShellArgs { /// Listen on a socket instead of stdin/stdout. #[clap(long)] pub on_socket: bool, + /// Listen on a port instead of stdin/stdout. + #[clap(long)] + pub on_port: bool, + /// Require the given token string to be given in the handshake. + #[clap(long)] + pub require_token: Option, } #[derive(Args, Debug, Clone)] @@ -634,6 +640,10 @@ pub enum TunnelSubcommand { /// (Preview) Manages the tunnel when installed as a system service, #[clap(subcommand)] Service(TunnelServiceSubCommands), + + /// (Preview) Forwards local port using the dev tunnel + #[clap(hide = true)] + ForwardInternal(TunnelForwardArgs), } #[derive(Subcommand, Debug, Clone)] @@ -669,6 +679,16 @@ pub struct TunnelRenameArgs { pub name: String, } +#[derive(Args, Debug, Clone)] +pub struct TunnelForwardArgs { + /// One or more ports to forward. + pub ports: Vec, + + /// Login args -- used for convenience so the forwarding call is a single action. + #[clap(flatten)] + pub login: LoginArgs, +} + #[derive(Subcommand, Debug, Clone)] pub enum TunnelUserSubCommands { /// Log in to port forwarding service diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 9831de6e4269d..95f9b12a3d423 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -10,24 +10,32 @@ use serde::Serialize; use sha2::{Digest, Sha256}; use std::{str::FromStr, time::Duration}; use sysinfo::Pid; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + sync::watch, +}; use super::{ args::{ - AuthProvider, CliCore, CommandShellArgs, ExistingTunnelArgs, TunnelRenameArgs, - TunnelServeArgs, TunnelServiceSubCommands, TunnelUserSubCommands, + AuthProvider, CliCore, CommandShellArgs, ExistingTunnelArgs, TunnelForwardArgs, + TunnelRenameArgs, TunnelServeArgs, TunnelServiceSubCommands, TunnelUserSubCommands, }, CommandContext, }; use crate::{ - async_pipe::{get_socket_name, listen_socket_rw_stream, socket_stream_split}, + async_pipe::{get_socket_name, listen_socket_rw_stream, AsyncRWAccepter}, auth::Auth, - constants::{APPLICATION_NAME, TUNNEL_CLI_LOCK_NAME, TUNNEL_SERVICE_LOCK_NAME}, + constants::{ + APPLICATION_NAME, CONTROL_PORT, IS_A_TTY, TUNNEL_CLI_LOCK_NAME, TUNNEL_SERVICE_LOCK_NAME, + }, log, state::LauncherPaths, tunnels::{ code_server::CodeServerArgs, - create_service_manager, dev_tunnels, legal, + create_service_manager, + dev_tunnels::{self, DevTunnels}, + forwarding, legal, paths::get_all_servers, protocol, serve_stream, shutdown_signal::ShutdownRequest, @@ -35,7 +43,7 @@ use crate::{ singleton_server::{ make_singleton_server, start_singleton_server, BroadcastLogSink, SingletonServerArgs, }, - Next, ServeStreamParams, ServiceContainer, ServiceManager, + AuthRequired, Next, ServeStreamParams, ServiceContainer, ServiceManager, }, util::{ app_lock::AppMutex, @@ -128,36 +136,52 @@ pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Resul log: ctx.log, launcher_paths: ctx.paths, platform, - requires_auth: true, + requires_auth: args + .require_token + .map(AuthRequired::VSDAWithToken) + .unwrap_or(AuthRequired::VSDA), exit_barrier: ShutdownRequest::create_rx([ShutdownRequest::CtrlC]), code_server_args: (&ctx.args).into(), }; - if !args.on_socket { - serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; - return Ok(0); - } + let mut listener: Box = match (args.on_port, args.on_socket) { + (_, true) => { + let socket = get_socket_name(); + let listener = listen_socket_rw_stream(&socket) + .await + .map_err(|e| wrap(e, "error listening on socket"))?; - let socket = get_socket_name(); - let mut listener = listen_socket_rw_stream(&socket) - .await - .map_err(|e| wrap(e, "error listening on socket"))?; + params + .log + .result(format!("Listening on {}", socket.display())); - params - .log - .result(format!("Listening on {}", socket.display())); + Box::new(listener) + } + (true, _) => { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .map_err(|e| wrap(e, "error listening on port"))?; + + params + .log + .result(format!("Listening on {}", listener.local_addr().unwrap())); + + Box::new(listener) + } + _ => { + serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; + return Ok(0); + } + }; let mut servers = FuturesUnordered::new(); loop { tokio::select! { Some(_) = servers.next() => {}, - socket = listener.accept() => { + socket = listener.accept_rw() => { match socket { - Ok(s) => { - let (read, write) = socket_stream_split(s); - servers.push(serve_stream(read, write, params.clone())); - }, + Ok((read, write)) => servers.push(serve_stream(read, write, params.clone())), Err(e) => { error!(params.log, &format!("Error accepting connection: {}", e)); return Ok(1); @@ -184,7 +208,7 @@ pub async fn service( if let Some(name) = &args.name { // ensure the name matches, and tunnel exists - dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths) + dev_tunnels::DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths) .rename_tunnel(name) .await?; } else { @@ -258,7 +282,7 @@ pub async fn user(ctx: CommandContext, user_args: TunnelUserSubCommands) -> Resu /// Remove the tunnel used by this tunnel, if any. pub async fn rename(ctx: CommandContext, rename_args: TunnelRenameArgs) -> Result { let auth = Auth::new(&ctx.paths, ctx.log.clone()); - let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths); + let mut dt = dev_tunnels::DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths); dt.rename_tunnel(&rename_args.name).await?; ctx.log.result(format!( "Successfully renamed this tunnel to {}", @@ -271,7 +295,7 @@ pub async fn rename(ctx: CommandContext, rename_args: TunnelRenameArgs) -> Resul /// Remove the tunnel used by this tunnel, if any. pub async fn unregister(ctx: CommandContext) -> Result { let auth = Auth::new(&ctx.paths, ctx.log.clone()); - let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths); + let mut dt = dev_tunnels::DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths); dt.remove_tunnel().await?; Ok(0) } @@ -379,6 +403,88 @@ pub async fn serve(ctx: CommandContext, gateway_args: TunnelServeArgs) -> Result result } +/// Internal command used by port forwarding. It reads requests for forwarded ports +/// on lines from stdin, as JSON. It uses singleton logic as well (though on +/// a different tunnel than the main one used for the control server) so that +/// all forward requests on a single machine go through a single hosted tunnel +/// process. Without singleton logic, requests could get routed to processes +/// that aren't forwarding a given port and then fail. +pub async fn forward( + ctx: CommandContext, + mut forward_args: TunnelForwardArgs, +) -> Result { + // Spooky: check IS_A_TTY before starting the stdin reader, since IS_A_TTY will + // access stdin but a lock will later be held on stdin by the line-reader. + if *IS_A_TTY { + trace!(ctx.log, "port forwarding is an internal preview feature"); + } + + // #region stdin reading logic: + let (own_ports_tx, own_ports_rx) = watch::channel(vec![]); + let ports_process_log = ctx.log.clone(); + tokio::spawn(async move { + let mut lines = BufReader::new(tokio::io::stdin()).lines(); + while let Ok(Some(line)) = lines.next_line().await { + match serde_json::from_str(&line) { + Ok(p) => { + let _ = own_ports_tx.send(p); + } + Err(e) => warning!(ports_process_log, "error parsing ports: {}", e), + } + } + }); + + // #region singleton acquisition + let shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]); + let server = loop { + if shutdown.is_open() { + return Ok(0); + } + + match acquire_singleton(&ctx.paths.forwarding_lockfile()).await { + Ok(SingletonConnection::Client(stream)) => { + debug!(ctx.log, "starting as client to singleton"); + let r = forwarding::client(forwarding::SingletonClientArgs { + log: ctx.log.clone(), + shutdown: shutdown.clone(), + stream, + port_requests: own_ports_rx.clone(), + }) + .await; + if let Err(e) = r { + warning!(ctx.log, "error contacting forwarding singleton: {}", e); + } + } + Ok(SingletonConnection::Singleton(server)) => break server, + Err(e) => { + warning!(ctx.log, "error access singleton, retrying: {}", e); + tokio::time::sleep(Duration::from_secs(2)).await + } + } + }; + + // #region singleton handler + let auth = Auth::new(&ctx.paths, ctx.log.clone()); + println!("preauth {:?}", forward_args.login); + if let (Some(p), Some(at)) = ( + forward_args.login.provider.take(), + forward_args.login.access_token.take(), + ) { + auth.login(Some(p.into()), Some(at)).await?; + } + println!("auth done"); + + let mut tunnels = DevTunnels::new_port_forwarding(&ctx.log, auth, &ctx.paths); + let tunnel = tunnels + .start_new_launcher_tunnel(None, true, &forward_args.ports) + .await?; + println!("made tunnel"); + + forwarding::server(ctx.log, tunnel, server, own_ports_rx, shutdown).await?; + + Ok(0) +} + fn get_connection_token(tunnel: &ActiveTunnel) -> String { let mut hash = Sha256::new(); hash.update(tunnel.id.as_bytes()); @@ -426,7 +532,7 @@ async fn serve_with_csa( return Ok(0); } - match acquire_singleton(paths.tunnel_lockfile()).await { + match acquire_singleton(&paths.tunnel_lockfile()).await { Ok(SingletonConnection::Client(stream)) => { debug!(log, "starting as client to singleton"); let should_exit = start_singleton_client(SingletonClientArgs { @@ -455,15 +561,19 @@ async fn serve_with_csa( let _lock = app_mutex_name.map(AppMutex::new); let auth = Auth::new(&paths, log.clone()); - let mut dt = dev_tunnels::DevTunnels::new(&log, auth, &paths); + let mut dt = dev_tunnels::DevTunnels::new_remote_tunnel(&log, auth, &paths); loop { let tunnel = if let Some(t) = fulfill_existing_tunnel_args(gateway_args.tunnel.clone(), &gateway_args.name) { dt.start_existing_tunnel(t).await } else { - dt.start_new_launcher_tunnel(gateway_args.name.as_deref(), gateway_args.random_name) - .await + dt.start_new_launcher_tunnel( + gateway_args.name.as_deref(), + gateway_args.random_name, + &[CONTROL_PORT], + ) + .await }?; csa.connection_token = Some(get_connection_token(&tunnel)); diff --git a/cli/src/msgpack_rpc.rs b/cli/src/msgpack_rpc.rs index 219c923cdf2ab..ef6b7782074c3 100644 --- a/cli/src/msgpack_rpc.rs +++ b/cli/src/msgpack_rpc.rs @@ -122,7 +122,7 @@ pub struct MsgPackCodec { impl MsgPackCodec { pub fn new() -> Self { Self { - _marker: std::marker::PhantomData::default(), + _marker: std::marker::PhantomData, } } } diff --git a/cli/src/singleton.rs b/cli/src/singleton.rs index 0ea9cda2a8a43..635c400fb0f6d 100644 --- a/cli/src/singleton.rs +++ b/cli/src/singleton.rs @@ -53,12 +53,12 @@ struct LockFileMatter { /// Tries to acquire the singleton homed at the given lock file, either starting /// a new singleton if it doesn't exist, or connecting otherwise. -pub async fn acquire_singleton(lock_file: PathBuf) -> Result { +pub async fn acquire_singleton(lock_file: &Path) -> Result { let file = OpenOptions::new() .read(true) .write(true) .create(true) - .open(&lock_file) + .open(lock_file) .map_err(CodeError::SingletonLockfileOpenFailed)?; match FileLock::acquire(file) { @@ -158,7 +158,7 @@ mod tests { #[tokio::test] async fn test_acquires_singleton() { let dir = tempfile::tempdir().expect("expected to make temp dir"); - let s = acquire_singleton(dir.path().join("lock")) + let s = acquire_singleton(&dir.path().join("lock")) .await .expect("expected to acquire"); @@ -172,7 +172,7 @@ mod tests { async fn test_acquires_client() { let dir = tempfile::tempdir().expect("expected to make temp dir"); let lockfile = dir.path().join("lock"); - let s1 = acquire_singleton(lockfile.clone()) + let s1 = acquire_singleton(&lockfile) .await .expect("expected to acquire1"); match s1 { @@ -182,7 +182,7 @@ mod tests { _ => panic!("expected to be singleton"), }; - let s2 = acquire_singleton(lockfile) + let s2 = acquire_singleton(&lockfile) .await .expect("expected to acquire2"); match s2 { diff --git a/cli/src/state.rs b/cli/src/state.rs index af0d18e160b34..1b1ff343da5cd 100644 --- a/cli/src/state.rs +++ b/cli/src/state.rs @@ -187,6 +187,14 @@ impl LauncherPaths { )) } + /// Lockfile for port forwarding + pub fn forwarding_lockfile(&self) -> PathBuf { + self.root.join(format!( + "forwarding-{}.lock", + VSCODE_CLI_QUALITY.unwrap_or("oss") + )) + } + /// Suggested path for tunnel service logs, when using file logs pub fn service_log_file(&self) -> PathBuf { self.root.join("tunnel-service.log") diff --git a/cli/src/tunnels.rs b/cli/src/tunnels.rs index 5d97b757afc1d..700516abb1f75 100644 --- a/cli/src/tunnels.rs +++ b/cli/src/tunnels.rs @@ -11,6 +11,7 @@ pub mod protocol; pub mod shutdown_signal; pub mod singleton_client; pub mod singleton_server; +pub mod forwarding; mod wsl_detect; mod challenge; @@ -34,7 +35,7 @@ mod service_macos; mod service_windows; mod socket_signal; -pub use control_server::{serve, serve_stream, Next, ServeStreamParams}; +pub use control_server::{serve, serve_stream, Next, ServeStreamParams, AuthRequired}; pub use nosleep::SleepInhibitor; pub use service::{ create_service_manager, ServiceContainer, ServiceManager, SERVICE_LOG_FILE_NAME, diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index 8577f9668e9a4..6f8c1060e1f44 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -48,11 +48,11 @@ use super::dev_tunnels::ActiveTunnel; use super::paths::prune_stopped_servers; use super::port_forwarder::{PortForwarding, PortForwardingProcessor}; use super::protocol::{ - AcquireCliParams, CallServerHttpParams, CallServerHttpResult, ChallengeIssueResponse, - ChallengeVerifyParams, ClientRequestMethod, EmptyObject, ForwardParams, ForwardResult, - FsStatRequest, FsStatResponse, GetEnvResponse, GetHostnameResponse, HttpBodyParams, - HttpHeadersParams, ServeParams, ServerLog, ServerMessageParams, SpawnParams, SpawnResult, - ToClientRequest, UnforwardParams, UpdateParams, UpdateResult, VersionResponse, + AcquireCliParams, CallServerHttpParams, CallServerHttpResult, ChallengeIssueParams, + ChallengeIssueResponse, ChallengeVerifyParams, ClientRequestMethod, EmptyObject, ForwardParams, + ForwardResult, FsStatRequest, FsStatResponse, GetEnvResponse, GetHostnameResponse, + HttpBodyParams, HttpHeadersParams, ServeParams, ServerLog, ServerMessageParams, SpawnParams, + SpawnResult, ToClientRequest, UnforwardParams, UpdateParams, UpdateResult, VersionResponse, METHOD_CHALLENGE_VERIFY, }; use super::server_bridge::ServerBridge; @@ -94,8 +94,8 @@ struct HandlerContext { /// Handler auth state. enum AuthState { - /// Auth is required, we're waiting for the client to send its challenge. - WaitingForChallenge, + /// Auth is required, we're waiting for the client to send its challenge optionally bearing a token. + WaitingForChallenge(Option), /// A challenge has been issued. Waiting for a verification. ChallengeIssued(String), /// Auth is no longer required. @@ -215,7 +215,7 @@ pub async fn serve( code_server_args: own_code_server_args, platform, exit_barrier: own_exit, - requires_auth: false, + requires_auth: AuthRequired::None, }).with_context(cx.clone()).await; cx.span().add_event( @@ -233,13 +233,20 @@ pub async fn serve( } } +#[derive(Clone)] +pub enum AuthRequired { + None, + VSDA, + VSDAWithToken(String), +} + #[derive(Clone)] pub struct ServeStreamParams { pub log: log::Logger, pub launcher_paths: LauncherPaths, pub code_server_args: CodeServerArgs, pub platform: Platform, - pub requires_auth: bool, + pub requires_auth: AuthRequired, pub exit_barrier: Barrier, } @@ -269,7 +276,7 @@ fn make_socket_rpc( launcher_paths: LauncherPaths, code_server_args: CodeServerArgs, port_forwarding: Option, - requires_auth: bool, + requires_auth: AuthRequired, platform: Platform, ) -> RpcDispatcher { let http_requests = Arc::new(std::sync::Mutex::new(HashMap::new())); @@ -277,8 +284,9 @@ fn make_socket_rpc( let mut rpc = RpcBuilder::new(MsgPackSerializer {}).methods(HandlerContext { did_update: Arc::new(AtomicBool::new(false)), auth_state: Arc::new(std::sync::Mutex::new(match requires_auth { - true => AuthState::WaitingForChallenge, - false => AuthState::Authenticated, + AuthRequired::VSDAWithToken(t) => AuthState::WaitingForChallenge(Some(t)), + AuthRequired::VSDA => AuthState::WaitingForChallenge(None), + AuthRequired::None => AuthState::Authenticated, })), socket_tx, log: log.clone(), @@ -305,8 +313,8 @@ fn make_socket_rpc( ensure_auth(&c.auth_state)?; handle_get_env() }); - rpc.register_sync(METHOD_CHALLENGE_ISSUE, |_: EmptyObject, c| { - handle_challenge_issue(&c.auth_state) + rpc.register_sync(METHOD_CHALLENGE_ISSUE, |p: ChallengeIssueParams, c| { + handle_challenge_issue(p, &c.auth_state) }); rpc.register_sync(METHOD_CHALLENGE_VERIFY, |p: ChallengeVerifyParams, c| { handle_challenge_verify(p.response, &c.auth_state) @@ -423,6 +431,7 @@ async fn process_socket( let rx_counter = Arc::new(AtomicUsize::new(0)); let http_requests = Arc::new(std::sync::Mutex::new(HashMap::new())); + let already_authed = matches!(requires_auth, AuthRequired::None); let rpc = make_socket_rpc( log.clone(), socket_tx.clone(), @@ -440,7 +449,7 @@ async fn process_socket( let socket_tx = socket_tx.clone(); let exit_barrier = exit_barrier.clone(); tokio::spawn(async move { - if !requires_auth { + if already_authed { send_version(&socket_tx).await; } @@ -826,13 +835,22 @@ fn handle_get_env() -> Result { } fn handle_challenge_issue( + params: ChallengeIssueParams, auth_state: &Arc>, ) -> Result { let challenge = create_challenge(); let mut auth_state = auth_state.lock().unwrap(); - *auth_state = AuthState::ChallengeIssued(challenge.clone()); + if let AuthState::WaitingForChallenge(Some(s)) = &*auth_state { + println!("looking for token {}, got {:?}", s, params.token); + match ¶ms.token { + Some(t) if s != t => return Err(CodeError::AuthChallengeBadToken.into()), + None => return Err(CodeError::AuthChallengeBadToken.into()), + _ => {} + } + } + *auth_state = AuthState::ChallengeIssued(challenge.clone()); Ok(ChallengeIssueResponse { challenge }) } @@ -844,7 +862,7 @@ fn handle_challenge_verify( match &*auth_state { AuthState::Authenticated => Ok(EmptyObject {}), - AuthState::WaitingForChallenge => Err(CodeError::AuthChallengeNotIssued.into()), + AuthState::WaitingForChallenge(_) => Err(CodeError::AuthChallengeNotIssued.into()), AuthState::ChallengeIssued(c) => match verify_challenge(c, &response) { false => Err(CodeError::AuthChallengeNotIssued.into()), true => { diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index a04bdbcaf6d34..3bdc5cf189bed 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ use crate::auth; -use crate::constants::{ - CONTROL_PORT, IS_INTERACTIVE_CLI, PROTOCOL_VERSION_TAG, TUNNEL_SERVICE_USER_AGENT, -}; +use crate::constants::{IS_INTERACTIVE_CLI, PROTOCOL_VERSION_TAG, TUNNEL_SERVICE_USER_AGENT}; use crate::state::{LauncherPaths, PersistedState}; use crate::util::errors::{ - wrap, AnyError, DevTunnelError, InvalidTunnelName, TunnelCreationFailed, WrappedError, + wrap, AnyError, CodeError, DevTunnelError, InvalidTunnelName, TunnelCreationFailed, + WrappedError, }; use crate::util::input::prompt_placeholder; use crate::{debug, info, log, spanf, trace, warning}; @@ -33,6 +32,8 @@ use tunnels::management::{ use super::wsl_detect::is_wsl_installed; +static TUNNEL_COUNT_LIMIT_NAME: &str = "TunnelsPerUserPerLocation"; + #[derive(Clone, Serialize, Deserialize)] pub struct PersistedTunnel { pub name: String, @@ -134,6 +135,7 @@ pub struct DevTunnels { log: log::Logger, launcher_tunnel: PersistedState>, client: TunnelManagementClient, + tag: &'static str, } /// Representation of a tunnel returned from the `start` methods. @@ -162,30 +164,43 @@ impl ActiveTunnel { } /// Forwards a port over TCP. - pub async fn add_port_tcp(&mut self, port_number: u16) -> Result<(), AnyError> { + pub async fn add_port_tcp(&self, port_number: u16) -> Result<(), AnyError> { self.manager.add_port_tcp(port_number).await?; Ok(()) } /// Removes a forwarded port TCP. - pub async fn remove_port(&mut self, port_number: u16) -> Result<(), AnyError> { + pub async fn remove_port(&self, port_number: u16) -> Result<(), AnyError> { self.manager.remove_port(port_number).await?; Ok(()) } + /// Gets the template string for forming forwarded port web URIs.. + pub fn get_port_format(&self) -> Result { + if let Some(details) = &*self.manager.endpoint_rx.borrow() { + return details + .as_ref() + .map(|r| { + r.base + .port_uri_format + .clone() + .expect("expected to have port format") + }) + .map_err(|e| e.clone().into()); + } + + Err(CodeError::NoTunnelEndpoint.into()) + } + /// Gets the public URI on which a forwarded port can be access in browser. - pub async fn get_port_uri(&mut self, port: u16) -> Result { - let endpoint = self.manager.get_endpoint().await?; - let format = endpoint - .base - .port_uri_format - .expect("expected to have port format"); - - Ok(format.replace(PORT_TOKEN, &port.to_string())) + pub fn get_port_uri(&self, port: u16) -> Result { + self.get_port_format() + .map(|f| f.replace(PORT_TOKEN, &port.to_string())) } } const VSCODE_CLI_TUNNEL_TAG: &str = "vscode-server-launcher"; +const VSCODE_CLI_FORWARDING_TAG: &str = "vscode-port-forward"; const MAX_TUNNEL_NAME_LENGTH: usize = 20; fn get_host_token_from_tunnel(tunnel: &Tunnel) -> String { @@ -242,7 +257,29 @@ pub struct ExistingTunnel { } impl DevTunnels { - pub fn new(log: &log::Logger, auth: auth::Auth, paths: &LauncherPaths) -> DevTunnels { + /// Creates a new DevTunnels client used for port forwarding. + pub fn new_port_forwarding( + log: &log::Logger, + auth: auth::Auth, + paths: &LauncherPaths, + ) -> DevTunnels { + let mut client = new_tunnel_management(&TUNNEL_SERVICE_USER_AGENT); + client.authorization_provider(auth); + + DevTunnels { + log: log.clone(), + client: client.into(), + launcher_tunnel: PersistedState::new(paths.root().join("port_forwarding_tunnel.json")), + tag: VSCODE_CLI_FORWARDING_TAG, + } + } + + /// Creates a new DevTunnels client used for the Remote Tunnels extension to access the VS Code Server. + pub fn new_remote_tunnel( + log: &log::Logger, + auth: auth::Auth, + paths: &LauncherPaths, + ) -> DevTunnels { let mut client = new_tunnel_management(&TUNNEL_SERVICE_USER_AGENT); client.authorization_provider(auth); @@ -250,6 +287,7 @@ impl DevTunnels { log: log.clone(), client: client.into(), launcher_tunnel: PersistedState::new(paths.root().join("code_tunnel.json")), + tag: VSCODE_CLI_TUNNEL_TAG, } } @@ -364,6 +402,7 @@ impl DevTunnels { &mut self, preferred_name: Option<&str>, use_random_name: bool, + preserve_ports: &[u16], ) -> Result { let (mut tunnel, persisted) = match self.launcher_tunnel.load() { Some(mut persisted) => { @@ -407,7 +446,7 @@ impl DevTunnels { for port_to_delete in tunnel .ports .iter() - .filter(|p| p.port_number != CONTROL_PORT) + .filter(|p: &&TunnelPort| !preserve_ports.contains(&p.port_number)) { let output_fut = self.client.delete_tunnel_port( &locator, @@ -458,14 +497,8 @@ impl DevTunnels { self.check_is_name_free(name).await?; - let mut tried_recycle = false; - let new_tunnel = Tunnel { - tags: vec![ - name.to_string(), - PROTOCOL_VERSION_TAG.to_string(), - VSCODE_CLI_TUNNEL_TAG.to_string(), - ], + tags: self.get_tags(name), ..Default::default() }; @@ -480,13 +513,14 @@ impl DevTunnels { Err(HttpError::ResponseError(e)) if e.status_code == StatusCode::TOO_MANY_REQUESTS => { - if !tried_recycle && self.try_recycle_tunnel().await? { - tried_recycle = true; - continue; - } - if let Some(d) = e.get_details() { let detail = d.detail.unwrap_or_else(|| "unknown".to_string()); + if detail.contains(TUNNEL_COUNT_LIMIT_NAME) + && self.try_recycle_tunnel().await? + { + continue; + } + return Err(AnyError::from(TunnelCreationFailed( name.to_string(), detail, @@ -523,7 +557,7 @@ impl DevTunnels { let mut tags = vec![ name.to_string(), PROTOCOL_VERSION_TAG.to_string(), - VSCODE_CLI_TUNNEL_TAG.to_string(), + self.tag.to_string(), ]; if is_wsl_installed(&self.log) { @@ -615,7 +649,7 @@ impl DevTunnels { self.log, self.log.span("dev-tunnel.listall"), self.client.list_all_tunnels(&TunnelRequestOptions { - tags: vec![VSCODE_CLI_TUNNEL_TAG.to_string()], + tags: vec![self.tag.to_string()], require_all_tags: true, ..Default::default() }) @@ -630,7 +664,7 @@ impl DevTunnels { self.log, self.log.span("dev-tunnel.rename.search"), self.client.list_all_tunnels(&TunnelRequestOptions { - tags: vec![VSCODE_CLI_TUNNEL_TAG.to_string(), name.to_string()], + tags: vec![self.tag.to_string(), name.to_string()], require_all_tags: true, ..Default::default() }) diff --git a/cli/src/tunnels/forwarding.rs b/cli/src/tunnels/forwarding.rs new file mode 100644 index 0000000000000..1557b97c04e03 --- /dev/null +++ b/cli/src/tunnels/forwarding.rs @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use tokio::{ + pin, + sync::{mpsc, watch}, +}; + +use crate::{ + async_pipe::{socket_stream_split, AsyncPipe}, + json_rpc::{new_json_rpc, start_json_rpc}, + log, + singleton::SingletonServer, + util::{errors::CodeError, sync::Barrier}, +}; + +use super::{ + dev_tunnels::ActiveTunnel, + protocol::{ + self, + forward_singleton::{PortList, SetPortsResponse}, + }, + shutdown_signal::ShutdownSignal, +}; + +type PortMap = HashMap; + +/// The PortForwardingHandle is given out to multiple consumers to allow +/// them to set_ports that they want to be forwarded. +struct PortForwardingSender { + /// Todo: when `SyncUnsafeCell` is no longer nightly, we can use it here with + /// the following comment: + /// + /// SyncUnsafeCell is used and safe here because PortForwardingSender is used + /// exclusively in synchronous dispatch *and* we create a new sender in the + /// context for each connection, in `serve_singleton_rpc`. + /// + /// If PortForwardingSender is ever used in a different context, this should + /// be refactored, e.g. to use locks or `&mut self` in set_ports` + /// + /// see https://doc.rust-lang.org/stable/std/cell/struct.SyncUnsafeCell.html + current: Mutex, + sender: Arc>>, +} + +impl PortForwardingSender { + pub fn set_ports(&self, ports: PortList) { + let mut current = self.current.lock().unwrap(); + self.sender.lock().unwrap().send_modify(|v| { + for p in current.iter() { + if !ports.contains(p) { + match v.get(p) { + Some(1) => { + v.remove(p); + } + Some(n) => { + v.insert(*p, n - 1); + } + None => unreachable!("removed port not in map"), + } + } + } + + for p in ports.iter() { + if !current.contains(p) { + match v.get(p) { + Some(n) => v.insert(*p, n + 1), + None => v.insert(*p, 1), + }; + } + } + + current.splice(.., ports); + }); + } +} + +impl Clone for PortForwardingSender { + fn clone(&self) -> Self { + Self { + current: Mutex::new(vec![]), + sender: self.sender.clone(), + } + } +} + +impl Drop for PortForwardingSender { + fn drop(&mut self) { + self.set_ports(vec![]); + } +} + +struct PortForwardingReceiver { + receiver: watch::Receiver, +} + +impl PortForwardingReceiver { + pub fn new() -> (PortForwardingSender, Self) { + let (sender, receiver) = watch::channel(HashMap::new()); + let handle = PortForwardingSender { + current: Mutex::new(vec![]), + sender: Arc::new(Mutex::new(sender)), + }; + + let tracker = Self { receiver }; + + (handle, tracker) + } + + /// Applies all changes from PortForwardingHandles to the tunnel. + pub async fn apply_to(&mut self, log: log::Logger, tunnel: Arc) { + let mut current = vec![]; + while self.receiver.changed().await.is_ok() { + let next = self.receiver.borrow().keys().copied().collect::>(); + + for p in current.iter() { + if !next.contains(p) { + match tunnel.remove_port(*p).await { + Ok(_) => info!(log, "stopped forwarding port {}", p), + Err(e) => error!(log, "failed to stop forwarding port {}: {}", p, e), + } + } + } + for p in next.iter() { + if !current.contains(p) { + match tunnel.add_port_tcp(*p).await { + Ok(_) => info!(log, "forwarding port {}", p), + Err(e) => error!(log, "failed to forward port {}: {}", p, e), + } + } + } + + current = next; + } + } +} + +pub struct SingletonClientArgs { + pub log: log::Logger, + pub stream: AsyncPipe, + pub shutdown: Barrier, + pub port_requests: watch::Receiver, +} + +#[derive(Clone)] +struct SingletonServerContext { + log: log::Logger, + handle: PortForwardingSender, + tunnel: Arc, +} + +/// Serves a client singleton for port forwarding. +pub async fn client(args: SingletonClientArgs) -> Result<(), std::io::Error> { + let mut rpc = new_json_rpc(); + let (msg_tx, msg_rx) = mpsc::unbounded_channel(); + let SingletonClientArgs { + log, + shutdown, + stream, + mut port_requests, + } = args; + + debug!( + log, + "An existing port forwarding process is running on this machine, connecting to it..." + ); + + let caller = rpc.get_caller(msg_tx); + let rpc = rpc.methods(()).build(log.clone()); + let (read, write) = socket_stream_split(stream); + + let serve = start_json_rpc(rpc, read, write, msg_rx, shutdown); + let forward = async move { + while port_requests.changed().await.is_ok() { + let ports = port_requests.borrow().clone(); + let r = caller + .call::<_, _, protocol::forward_singleton::SetPortsResponse>( + protocol::forward_singleton::METHOD_SET_PORTS, + protocol::forward_singleton::SetPortsParams { ports }, + ) + .await + .unwrap(); + + match r { + Err(e) => error!(log, "failed to set ports: {:?}", e), + Ok(r) => print_forwarding_addr(&r), + }; + } + }; + + tokio::select! { + r = serve => r.map(|_| ()), + _ = forward => Ok(()), + } +} + +/// Serves a port-forwarding singleton. +pub async fn server( + log: log::Logger, + tunnel: ActiveTunnel, + server: SingletonServer, + mut port_requests: watch::Receiver, + shutdown_rx: Barrier, +) -> Result<(), CodeError> { + let tunnel = Arc::new(tunnel); + let (forward_tx, mut forward_rx) = PortForwardingReceiver::new(); + + let forward_own_tunnel = tunnel.clone(); + let forward_own_tx = forward_tx.clone(); + let forward_own = async move { + while port_requests.changed().await.is_ok() { + forward_own_tx.set_ports(port_requests.borrow().clone()); + print_forwarding_addr(&SetPortsResponse { + port_format: forward_own_tunnel.get_port_format().ok(), + }); + } + }; + + tokio::select! { + _ = forward_own => Ok(()), + _ = forward_rx.apply_to(log.clone(), tunnel.clone()) => Ok(()), + r = serve_singleton_rpc(server, log, tunnel, forward_tx, shutdown_rx) => r, + } +} + +async fn serve_singleton_rpc( + mut server: SingletonServer, + log: log::Logger, + tunnel: Arc, + forward_tx: PortForwardingSender, + shutdown_rx: Barrier, +) -> Result<(), CodeError> { + let mut own_shutdown = shutdown_rx.clone(); + let shutdown_fut = own_shutdown.wait(); + pin!(shutdown_fut); + + loop { + let cnx = tokio::select! { + c = server.accept() => c?, + _ = &mut shutdown_fut => return Ok(()), + }; + + let (read, write) = socket_stream_split(cnx); + let shutdown_rx = shutdown_rx.clone(); + + let handle = forward_tx.clone(); + let log = log.clone(); + let tunnel = tunnel.clone(); + tokio::spawn(async move { + // we make an rpc for the connection instead of re-using a dispatcher + // so that we can have the "handle" drop when the connection drops. + let rpc = new_json_rpc(); + let mut rpc = rpc.methods(SingletonServerContext { + log: log.clone(), + handle, + tunnel, + }); + + rpc.register_sync( + protocol::forward_singleton::METHOD_SET_PORTS, + |p: protocol::forward_singleton::SetPortsParams, ctx| { + info!(ctx.log, "client setting ports to {:?}", p.ports); + ctx.handle.set_ports(p.ports); + Ok(SetPortsResponse { + port_format: ctx.tunnel.get_port_format().ok(), + }) + }, + ); + + let _ = start_json_rpc(rpc.build(log), read, write, (), shutdown_rx).await; + }); + } +} + +fn print_forwarding_addr(r: &SetPortsResponse) { + eprintln!("{}\n", serde_json::to_string(r).unwrap()); +} diff --git a/cli/src/tunnels/port_forwarder.rs b/cli/src/tunnels/port_forwarder.rs index bb60a670dea9f..0f489f541d7c5 100644 --- a/cli/src/tunnels/port_forwarder.rs +++ b/cli/src/tunnels/port_forwarder.rs @@ -91,7 +91,7 @@ impl PortForwardingProcessor { self.forwarded.insert(port); } - tunnel.get_port_uri(port).await + tunnel.get_port_uri(port) } } diff --git a/cli/src/tunnels/protocol.rs b/cli/src/tunnels/protocol.rs index eb20afe0ce588..cf01917ee1eca 100644 --- a/cli/src/tunnels/protocol.rs +++ b/cli/src/tunnels/protocol.rs @@ -199,6 +199,11 @@ pub struct SpawnResult { pub const METHOD_CHALLENGE_ISSUE: &str = "challenge_issue"; pub const METHOD_CHALLENGE_VERIFY: &str = "challenge_verify"; +#[derive(Serialize, Deserialize)] +pub struct ChallengeIssueParams { + pub token: Option, +} + #[derive(Serialize, Deserialize)] pub struct ChallengeIssueResponse { pub challenge: String, @@ -209,6 +214,24 @@ pub struct ChallengeVerifyParams { pub response: String, } +pub mod forward_singleton { + use serde::{Deserialize, Serialize}; + + pub const METHOD_SET_PORTS: &str = "set_ports"; + + pub type PortList = Vec; + + #[derive(Serialize, Deserialize)] + pub struct SetPortsParams { + pub ports: PortList, + } + + #[derive(Serialize, Deserialize)] + pub struct SetPortsResponse { + pub port_format: Option, + } +} + pub mod singleton { use crate::log; use serde::{Deserialize, Serialize}; diff --git a/cli/src/tunnels/singleton_server.rs b/cli/src/tunnels/singleton_server.rs index 703c09f51201e..77cc701ca46d0 100644 --- a/cli/src/tunnels/singleton_server.rs +++ b/cli/src/tunnels/singleton_server.rs @@ -217,7 +217,7 @@ impl BroadcastLogSink { } } - fn get_brocaster(&self) -> broadcast::Sender> { + pub fn get_brocaster(&self) -> broadcast::Sender> { self.tx.clone() } diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index ca6d4bf3d8a57..c82e14acc8b02 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -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. *--------------------------------------------------------------------------------------------*/ - use crate::{ +use crate::{ constants::{APPLICATION_NAME, CONTROL_PORT, DOCUMENTATION_URL, QUALITYLESS_PRODUCT_NAME}, rpc::ResponseError, }; @@ -509,10 +509,14 @@ pub enum CodeError { ServerAuthRequired, #[error("challenge not yet issued")] AuthChallengeNotIssued, + #[error("challenge token is invalid")] + AuthChallengeBadToken, #[error("unauthorized client refused")] AuthMismatch, #[error("keyring communication timed out after 5s")] KeyringTimeout, + #[error("no host is connected to the tunnel relay")] + NoTunnelEndpoint, } makeAnyError!( diff --git a/extensions/emmet/src/test/abbreviationAction.test.ts b/extensions/emmet/src/test/abbreviationAction.test.ts index beb3bfa1ef3e6..17ccacfc94a52 100644 --- a/extensions/emmet/src/test/abbreviationAction.test.ts +++ b/extensions/emmet/src/test/abbreviationAction.test.ts @@ -410,7 +410,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { // }); // }); - test.skip('No expanding when html is excluded in the settings in completion list', async () => { + test('No expanding when html is excluded in the settings in completion list', async () => { const oldConfig = workspace.getConfiguration('emmet').inspect('excludeLanguages')?.globalValue; await workspace.getConfiguration('emmet').update('excludeLanguages', ['html'], ConfigurationTarget.Global); await testHtmlCompletionProvider(new Selection(9, 6, 9, 6), '', '', true); @@ -469,7 +469,7 @@ suite('Tests for jsx, xml and xsl', () => { }); }); - test.skip('Expand abbreviation with no self closing tags for html', () => { + test('Expand abbreviation with no self closing tags for html', () => { return withRandomFileEditor('img', 'html', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'html' }); diff --git a/extensions/html-language-features/server/src/test/folding.test.ts b/extensions/html-language-features/server/src/test/folding.test.ts index 44aaea9026cf5..ec33f7a5198f0 100644 --- a/extensions/html-language-features/server/src/test/folding.test.ts +++ b/extensions/html-language-features/server/src/test/folding.test.ts @@ -37,7 +37,7 @@ function r(startLine: number, endLine: number, kind?: string): ExpectedIndentRan return { startLine, endLine, kind }; } -suite('HTML Folding', async () => { +suite('HTML Folding', () => { test('Embedded JavaScript', async () => { const input = [ diff --git a/extensions/java/cgmanifest.json b/extensions/java/cgmanifest.json index a4db862ab7d50..d5e92ad349dab 100644 --- a/extensions/java/cgmanifest.json +++ b/extensions/java/cgmanifest.json @@ -4,13 +4,14 @@ "component": { "type": "git", "git": { - "name": "atom/language-java", - "repositoryUrl": "https://github.com/atom/language-java", - "commitHash": "29f977dc42a7e2568b39bb6fb34c4ef108eb59b3" + "name": "redhat-developer/vscode-java", + "repositoryUrl": "https://github.com/redhat-developer/vscode-java", + "commitHash": "7a770ab6750b4b09173d98de14eb9792e3432b36" } }, "license": "MIT", - "version": "0.32.1" + "description": "This grammar was derived from https://github.com/atom/language-java/blob/master/grammars/java.cson.", + "version": "1.21.0" } ], "version": 1 diff --git a/extensions/java/package.json b/extensions/java/package.json index d71aa1c146ccd..6788ddd213335 100644 --- a/extensions/java/package.json +++ b/extensions/java/package.json @@ -9,7 +9,7 @@ "vscode": "*" }, "scripts": { - "update-grammar": "node ../node_modules/vscode-grammar-updater/bin atom/language-java grammars/java.cson ./syntaxes/java.tmLanguage.json" + "update-grammar": "node ../node_modules/vscode-grammar-updater/bin redhat-developer/vscode-java language-support/java/java.tmLanguage.json ./syntaxes/java.tmLanguage.json" }, "contributes": { "languages": [ diff --git a/extensions/java/syntaxes/java.tmLanguage.json b/extensions/java/syntaxes/java.tmLanguage.json index be70cbb27c372..6fa78eb890952 100644 --- a/extensions/java/syntaxes/java.tmLanguage.json +++ b/extensions/java/syntaxes/java.tmLanguage.json @@ -1,10 +1,10 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/atom/language-java/blob/master/grammars/java.cson", + "This file has been converted from https://github.com/redhat-developer/vscode-java/blob/master/language-support/java/java.tmLanguage.json", "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/atom/language-java/commit/29f977dc42a7e2568b39bb6fb34c4ef108eb59b3", + "version": "https://github.com/redhat-developer/vscode-java/commit/7a770ab6750b4b09173d98de14eb9792e3432b36", "name": "Java", "scopeName": "source.java", "patterns": [ @@ -1571,6 +1571,31 @@ }, "strings": { "patterns": [ + { + "begin": "\"\"\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.java" + } + }, + "end": "\"\"\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.java" + } + }, + "name": "string.quoted.triple.java", + "patterns": [ + { + "match": "\\\\\"\"\"", + "name": "constant.character.escape.java" + }, + { + "match": "\\\\.", + "name": "constant.character.escape.java" + } + ] + }, { "begin": "\"", "beginCaptures": { @@ -1632,6 +1657,9 @@ { "match": "[a-zA-Z$_][\\.a-zA-Z0-9$_]*", "name": "storage.type.java" + }, + { + "include": "#comments" } ] }, diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index f050f5a3162aa..d52d6b6b12a72 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -176,39 +176,42 @@ export const activate: ActivationFunction = (ctx) => { hr { border: 0; - height: 1px; - border-bottom: 1px solid; + height: 2px; + border-bottom: 2px solid; + } + + h2, h3, h4, h5, h6 { + font-weight: normal; } h1 { - font-size: 2em; - margin-top: 0; - padding-bottom: 0.3em; - border-bottom-width: 1px; - border-bottom-style: solid; + font-size: 2.3em; } h2 { - font-size: 1.5em; - padding-bottom: 0.3em; - border-bottom-width: 1px; - border-bottom-style: solid; + font-size: 2em; } h3 { - font-size: 1.25em; + font-size: 1.7em; + } + + h3 { + font-size: 1.5em; } h4 { - font-size: 1em; + font-size: 1.3em; } h5 { - font-size: 0.875em; + font-size: 1.2em; } - h6 { - font-size: 0.85em; + h1, + h2, + h3 { + font-weight: normal; } div { @@ -226,38 +229,12 @@ export const activate: ActivationFunction = (ctx) => { } /* Removes bottom margin when only one item exists in markdown cell */ - #preview > *:not(h1):not(h2):only-child, - #preview > *:not(h1):not(h2):last-child { + #preview > *:only-child, + #preview > *:last-child { margin-bottom: 0; padding-bottom: 0; } - h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 600; - margin-top: 24px; - margin-bottom: 16px; - line-height: 1.25; - } - - .vscode-light h1, - .vscode-light h2, - .vscode-light hr, - .vscode-light td { - border-color: rgba(0, 0, 0, 0.18); - } - - .vscode-dark h1, - .vscode-dark h2, - .vscode-dark hr, - .vscode-dark td { - border-color: rgba(255, 255, 255, 0.18); - } - /* makes all markdown cells consistent */ div { min-height: var(--notebook-markdown-min-height); diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 0468fbf1d7946..7336657621ee8 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -44,9 +44,9 @@ "configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.", "configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.", "configuration.markdown.editor.pasteUrlAsFormattedLink.enabled": "Controls how a Markdown link is created when a URL is pasted into the Markdown editor. Requires enabling `#editor.pasteAs.enabled#`.", - "configuration.pasteUrlAsFormattedLink.always": "Always create a Markdown link when a URL is pasted into the Markdown editor.", - "configuration.pasteUrlAsFormattedLink.smart": "Does not create a Markdown link within a link snippet or code bracket.", - "configuration.pasteUrlAsFormattedLink.never": "Never create a Markdown link when a URL is pasted into the Markdown editor.", + "configuration.pasteUrlAsFormattedLink.always": "Always creates a Markdown link when a URL is pasted into the Markdown editor.", + "configuration.pasteUrlAsFormattedLink.smart": "Smartly avoids creating a Markdown link in specific cases, such as within code brackets or inside an existing Markdown link.", + "configuration.pasteUrlAsFormattedLink.never": "Never creates a Markdown link when a URL is pasted into the Markdown editor.", "configuration.markdown.validate.enabled.description": "Enable all error reporting in Markdown files.", "configuration.markdown.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, for example: `[link][ref]`. Requires enabling `#markdown.validate.enabled#`.", "configuration.markdown.validate.fragmentLinks.enabled.description": "Validate fragment links to headers in the current Markdown file, for example: `[link](#header)`. Requires enabling `#markdown.validate.enabled#`.", diff --git a/extensions/markdown-language-features/src/commands/insertResource.ts b/extensions/markdown-language-features/src/commands/insertResource.ts index da50c83c991c5..776899f0ac716 100644 --- a/extensions/markdown-language-features/src/commands/insertResource.ts +++ b/extensions/markdown-language-features/src/commands/insertResource.ts @@ -60,7 +60,7 @@ export class InsertImageFromWorkspace implements Command { } function getDefaultUri(document: vscode.TextDocument) { - const docUri = getParentDocumentUri(document); + const docUri = getParentDocumentUri(document.uri); if (docUri.scheme === Schemes.untitled) { return vscode.workspace.workspaceFolders?.[0]?.uri; } @@ -76,10 +76,10 @@ async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode await vscode.workspace.applyEdit(edit); } -function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean, title = '', placeholderValue = 0, smartPaste = false) { +function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean, title = '', placeholderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false) { const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => { const selectionText = activeEditor.document.getText(selection); - const snippet = createUriListSnippet(activeEditor.document, selectedFiles, title, placeholderValue, smartPaste, { + const snippet = createUriListSnippet(activeEditor.document, selectedFiles, title, placeholderValue, pasteAsMarkdownLink, isExternalLink, { insertAsMedia, placeholderText: selectionText, placeholderStartIndex: (i + 1) * selectedFiles.length, diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts index cb6a77e8c8d1e..c8f44fad2bccb 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts @@ -81,7 +81,7 @@ export class NewFilePathGenerator { } function getDesiredNewFilePath(config: CopyFileConfiguration, document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri { - const docUri = getParentDocumentUri(document); + const docUri = getParentDocumentUri(document.uri); for (const [rawGlob, rawDest] of Object.entries(config.destination)) { for (const glob of parseGlob(rawGlob)) { if (picomatch.isMatch(docUri.path, glob, { dot: true })) { diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts index 24e23758d2576..727216c02bc92 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { Schemes } from '../../util/schemes'; -import { createEditForMediaFiles, getMarkdownLink, mediaMimes } from './shared'; +import { createEditForMediaFiles, createEditAddingLinksForUriList, mediaMimes, getPasteUrlAsFormattedLinkSetting, PasteUrlAsFormattedLink } from './shared'; class PasteEditProvider implements vscode.DocumentPasteEditProvider { @@ -32,7 +32,8 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider { if (!urlList) { return; } - const pasteEdit = await getMarkdownLink(document, ranges, urlList, token); + const pasteUrlSetting = await getPasteUrlAsFormattedLinkSetting(document); + const pasteEdit = await createEditAddingLinksForUriList(document, ranges, urlList, false, pasteUrlSetting === PasteUrlAsFormattedLink.Smart, token); if (!pasteEdit) { return; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts index 90755d990fcac..e596e0c2c8229 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { getMarkdownLink } from './shared'; +import { externalUriSchemes, createEditAddingLinksForUriList, getPasteUrlAsFormattedLinkSetting, PasteUrlAsFormattedLink } from './shared'; class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider { readonly id = 'insertMarkdownLink'; @@ -14,25 +14,28 @@ class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider { dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken, ): Promise { - const enabled = vscode.workspace.getConfiguration('markdown', document).get<'always' | 'smart' | 'never'>('editor.pasteUrlAsFormattedLink.enabled', 'smart'); - if (enabled === 'never') { + const pasteUrlSetting = await getPasteUrlAsFormattedLinkSetting(document); + if (pasteUrlSetting === PasteUrlAsFormattedLink.Never) { return; } - // Check if dataTransfer contains a URL const item = dataTransfer.get('text/plain'); - try { - new URL(await item?.value); - } catch (error) { + const urlList = await item?.asString(); + + if (urlList === undefined) { + return; + } + + if (!validateLink(urlList).isValid) { return; } const uriEdit = new vscode.DocumentPasteEdit('', this.id, ''); - const urlList = await item?.asString(); if (!urlList) { return undefined; } - const pasteEdit = await getMarkdownLink(document, ranges, urlList, token); + + const pasteEdit = await createEditAddingLinksForUriList(document, ranges, validateLink(urlList).cleanedUrlList, true, pasteUrlSetting === PasteUrlAsFormattedLink.Smart, token); if (!pasteEdit) { return; } @@ -43,6 +46,22 @@ class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider { } } +export function validateLink(urlList: string): { isValid: boolean; cleanedUrlList: string } { + let isValid = false; + let uri = undefined; + const trimmedUrlList = urlList?.trim(); //remove leading and trailing whitespace and new lines + try { + uri = vscode.Uri.parse(trimmedUrlList); + } catch (error) { + return { isValid: false, cleanedUrlList: urlList }; + } + const splitUrlList = trimmedUrlList.split(' ').filter(item => item !== ''); //split on spaces and remove empty strings + if (uri) { + isValid = splitUrlList.length === 1 && !splitUrlList[0].includes('\n') && externalUriSchemes.includes(vscode.Uri.parse(splitUrlList[0]).scheme); + } + return { isValid, cleanedUrlList: splitUrlList[0] }; +} + export function registerLinkPasteSupport(selector: vscode.DocumentSelector,) { return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteLinkEditProvider(), { pasteMimeTypes: [ diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index f40fea3556fea..1a0d4f01ae737 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -17,9 +17,10 @@ enum MediaKind { Audio, } -const externalUriSchemes = [ +export const externalUriSchemes = [ 'http', 'https', + 'mailto', ]; export const mediaFileExtensions = new Map([ @@ -61,32 +62,83 @@ export const mediaMimes = new Set([ 'audio/x-wav', ]); -export async function getMarkdownLink(document: vscode.TextDocument, ranges: readonly vscode.Range[], urlList: string, token: vscode.CancellationToken): Promise<{ additionalEdits: vscode.WorkspaceEdit; label: string } | undefined> { +const smartPasteRegexes = [ + { regex: /\[.*\]\(.*\)/g, isMarkdownLink: true, isInline: true }, // Is a Markdown Link + { regex: /!\[.*\]\(.*\)/g, isMarkdownLink: true, isInline: true }, // Is a Markdown Image Link + { regex: /\[([^\]]*)\]\(([^)]*)\)/g, isMarkdownLink: false, isInline: true }, // In a Markdown link + { regex: /^```[\s\S]*?```$/gm, isMarkdownLink: false, isInline: false }, // In a fenced code block + { regex: /^\$\$[\s\S]*?\$\$$/gm, isMarkdownLink: false, isInline: false }, // In a fenced math block + { regex: /`[^`]*`/g, isMarkdownLink: false, isInline: true }, // In inline code + { regex: /\$[^$]*\$/g, isMarkdownLink: false, isInline: true }, // In inline math +]; + +export interface SkinnyTextDocument { + offsetAt(position: vscode.Position): number; + getText(range?: vscode.Range): string; + readonly uri: vscode.Uri; +} + +export interface SmartPaste { + + /** + * `true` if the link is not being pasted within a markdown link, code, or math. + */ + pasteAsMarkdownLink: boolean; + + /** + * `true` if the link is being pasted over a markdown link. + */ + updateTitle: boolean; + +} + +export enum PasteUrlAsFormattedLink { + Always = 'always', + Smart = 'smart', + Never = 'never' +} + +export async function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument): Promise { + return vscode.workspace.getConfiguration('markdown', document).get('editor.pasteUrlAsFormattedLink.enabled', PasteUrlAsFormattedLink.Smart); +} + +export async function createEditAddingLinksForUriList( + document: SkinnyTextDocument, + ranges: readonly vscode.Range[], + urlList: string, + isExternalLink: boolean, + useSmartPaste: boolean, + token: vscode.CancellationToken, +): Promise<{ additionalEdits: vscode.WorkspaceEdit; label: string } | undefined> { + if (ranges.length === 0) { return; } - const enabled = vscode.workspace.getConfiguration('markdown', document).get<'always' | 'smart' | 'never'>('editor.pasteUrlAsFormattedLink.enabled', 'always'); - const edits: vscode.SnippetTextEdit[] = []; let placeHolderValue: number = ranges.length; let label: string = ''; - let smartPaste: boolean = false; - for (let i = 0; i < ranges.length; i++) { - if (enabled === 'smart') { - const inMarkdownLink = checkPaste(document, ranges, /\[([^\]]*)\]\(([^)]*)\)/g, i); - const inFencedCode = checkPaste(document, ranges, /^```[\s\S]*?```$/gm, i); - const inFencedMath = checkPaste(document, ranges, /^\$\$[\s\S]*?\$\$$/gm, i); - smartPaste = (inMarkdownLink || inFencedCode || inFencedMath); + let smartPaste = { pasteAsMarkdownLink: true, updateTitle: false }; + + for (const range of ranges) { + let title = document.getText(range); + const selectedRange: vscode.Range = new vscode.Range( + new vscode.Position(range.start.line, document.offsetAt(range.start)), + new vscode.Position(range.end.line, document.offsetAt(range.end)) + ); + + if (useSmartPaste) { + smartPaste = checkSmartPaste(document, selectedRange); + title = smartPaste.updateTitle ? '' : document.getText(range); } - const snippet = await tryGetUriListSnippet(document, urlList, token, document.getText(ranges[i]), placeHolderValue, smartPaste); + const snippet = await tryGetUriListSnippet(document, urlList, token, title, placeHolderValue, smartPaste.pasteAsMarkdownLink, isExternalLink); if (!snippet) { return; } - smartPaste = false; + smartPaste.pasteAsMarkdownLink = true; placeHolderValue--; - edits.push(new vscode.SnippetTextEdit(ranges[i], snippet.snippet)); + edits.push(new vscode.SnippetTextEdit(range, snippet.snippet)); label = snippet.label; } @@ -96,20 +148,25 @@ export async function getMarkdownLink(document: vscode.TextDocument, ranges: rea return { additionalEdits, label }; } -function checkPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], regex: RegExp, index: number): boolean { - const rangeStartOffset = document.offsetAt(ranges[index].start); - const rangeEndOffset = document.offsetAt(ranges[index].end); - const matches = [...document.getText().matchAll(regex)]; - for (const match of matches) { - if (match.index !== undefined && rangeStartOffset > match.index && rangeEndOffset < match.index + match[0].length) { - return true; +export function checkSmartPaste(document: SkinnyTextDocument, selectedRange: vscode.Range): SmartPaste { + const SmartPaste: SmartPaste = { pasteAsMarkdownLink: true, updateTitle: false }; + for (const regex of smartPasteRegexes) { + const matches = [...document.getText().matchAll(regex.regex)]; + for (const match of matches) { + if (match.index !== undefined) { + const useDefaultPaste = selectedRange.start.character > match.index && selectedRange.end.character < match.index + match[0].length; + SmartPaste.pasteAsMarkdownLink = !useDefaultPaste; + SmartPaste.updateTitle = regex.isMarkdownLink && selectedRange.start.character === match.index && selectedRange.end.character === match.index + match[0].length; + if (!SmartPaste.pasteAsMarkdownLink || SmartPaste.updateTitle) { + return SmartPaste; + } + } } } - - return false; + return SmartPaste; } -export async function tryGetUriListSnippet(document: vscode.TextDocument, urlList: String, token: vscode.CancellationToken, title = '', placeHolderValue = 0, smartPaste = false): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> { +export async function tryGetUriListSnippet(document: SkinnyTextDocument, urlList: String, token: vscode.CancellationToken, title = '', placeHolderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> { if (token.isCancellationRequested) { return undefined; } @@ -123,7 +180,7 @@ export async function tryGetUriListSnippet(document: vscode.TextDocument, urlLis } } - return createUriListSnippet(document, uris, title, placeHolderValue, smartPaste); + return createUriListSnippet(document, uris, title, placeHolderValue, pasteAsMarkdownLink, isExternalLink); } interface UriListSnippetOptions { @@ -141,28 +198,48 @@ interface UriListSnippetOptions { readonly separator?: string; } +export function appendToLinkSnippet( + snippet: vscode.SnippetString, + pasteAsMarkdownLink: boolean, + mdPath: string, + title: string, + uri: vscode.Uri, + placeholderValue: number, + isExternalLink: boolean, +): vscode.SnippetString { + const uriString = uri.toString(true); + if (pasteAsMarkdownLink) { + snippet.appendText('['); + snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue); + snippet.appendText(isExternalLink ? `](${uriString})` : `](${escapeMarkdownLinkPath(mdPath)})`); + } else { + snippet.appendText(isExternalLink ? uriString : escapeMarkdownLinkPath(mdPath)); + } + return snippet; +} + export function createUriListSnippet( - document: vscode.TextDocument, + document: SkinnyTextDocument, uris: readonly vscode.Uri[], title = '', placeholderValue = 0, - smartPaste = false, + pasteAsMarkdownLink = true, + isExternalLink = false, options?: UriListSnippetOptions, ): { snippet: vscode.SnippetString; label: string } | undefined { if (!uris.length) { return; } - const dir = getDocumentDir(document); - - const snippet = new vscode.SnippetString(); + const documentDir = getDocumentDir(document.uri); + let snippet = new vscode.SnippetString(); let insertedLinkCount = 0; let insertedImageCount = 0; let insertedAudioVideoCount = 0; uris.forEach((uri, i) => { - const mdPath = getMdPath(dir, uri); + const mdPath = getMdPath(documentDir, uri); const ext = URI.Utils.extname(uri).toLowerCase().replace('.', ''); const insertAsMedia = typeof options?.insertAsMedia === 'undefined' ? mediaFileExtensions.has(ext) : !!options.insertAsMedia; @@ -179,33 +256,22 @@ export function createUriListSnippet( snippet.appendText(`'); - } else { + } else if (insertAsMedia) { if (insertAsMedia) { insertedImageCount++; - snippet.appendText('!['); - const placeholderText = escapeBrackets(title) || options?.placeholderText || 'Alt text'; - const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : (placeholderValue === 0 ? undefined : placeholderValue); - snippet.appendPlaceholder(placeholderText, placeholderIndex); - snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); - } else { - insertedLinkCount++; - if (smartPaste) { - if (externalUriSchemes.includes(uri.scheme)) { - snippet.appendText(uri.toString(true)); - } else { - snippet.appendText(escapeMarkdownLinkPath(mdPath)); - } + if (pasteAsMarkdownLink) { + snippet.appendText('!['); + const placeholderText = escapeBrackets(title) || options?.placeholderText || 'Alt text'; + const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : (placeholderValue === 0 ? undefined : placeholderValue); + snippet.appendPlaceholder(placeholderText, placeholderIndex); + snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); } else { - snippet.appendText('['); - snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue); - if (externalUriSchemes.includes(uri.scheme)) { - const uriString = uri.toString(true); - snippet.appendText(`](${uriString})`); - } else { - snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); - } + snippet.appendText(escapeMarkdownLinkPath(mdPath)); } } + } else { + insertedLinkCount++; + snippet = appendToLinkSnippet(snippet, pasteAsMarkdownLink, mdPath, title, uri, placeholderValue, isExternalLink); } if (i < uris.length - 1 && uris.length > 1) { diff --git a/extensions/markdown-language-features/src/test/markdownLink.test.ts b/extensions/markdown-language-features/src/test/markdownLink.test.ts new file mode 100644 index 0000000000000..8568f21d87b6f --- /dev/null +++ b/extensions/markdown-language-features/src/test/markdownLink.test.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import 'mocha'; +import { SkinnyTextDocument, checkSmartPaste, createEditAddingLinksForUriList, appendToLinkSnippet } from '../languageFeatures/copyFiles/shared'; +import { validateLink } from '../languageFeatures/copyFiles/copyPasteLinks'; +suite('createEditAddingLinksForUriList', () => { + + test('Markdown Link Pasting should occur for a valid link (end to end)', async () => { + // createEditAddingLinksForUriList -> checkSmartPaste -> tryGetUriListSnippet -> createUriListSnippet -> createLinkSnippet + + const skinnyDocument: SkinnyTextDocument = { + uri: vscode.Uri.parse('file:///path/to/your/file'), + offsetAt: function () { return 0; }, + getText: function () { return 'hello world!'; }, + // lineAt: function (position: vscode.Position) { + // return { + // lineNumber: 0, + // text: 'hello world!', + // range: new vscode.Range(position, position), + // rangeIncludingLineBreak: new vscode.Range(position, position), + // firstNonWhitespaceCharacterIndex: 0, + // isEmptyOrWhitespace: false + // } as vscode.TextLine; + // } + }; + + const result = await createEditAddingLinksForUriList(skinnyDocument, [new vscode.Range(0, 0, 0, 12)], 'https://www.microsoft.com/', true, true, new vscode.CancellationTokenSource().token); + // need to check the actual result -> snippet value + assert.strictEqual(result?.label, 'Insert Markdown Link'); + }); + + suite('validateLink', () => { + + test('Markdown pasting should occur for a valid link.', () => { + const isLink = validateLink('https://www.microsoft.com/').isValid; + assert.strictEqual(isLink, true); + }); + + test('Markdown pasting should occur for a valid link preceded by a new line.', () => { + const isLink = validateLink('\r\nhttps://www.microsoft.com/').isValid; + assert.strictEqual(isLink, true); + }); + + test('Markdown pasting should occur for a valid link followed by a new line.', () => { + const isLink = validateLink('https://www.microsoft.com/\r\n').isValid; + assert.strictEqual(isLink, true); + }); + + test('Markdown pasting should not occur for a valid hostname and invalid protool.', () => { + const isLink = validateLink('invalid:www.microsoft.com').isValid; + assert.strictEqual(isLink, false); + }); + + test('Markdown pasting should not occur for plain text.', () => { + const isLink = validateLink('hello world!').isValid; + assert.strictEqual(isLink, false); + }); + + test('Markdown pasting should not occur for plain text including a colon.', () => { + const isLink = validateLink('hello: world!').isValid; + assert.strictEqual(isLink, false); + }); + + test('Markdown pasting should not occur for plain text including a slashes.', () => { + const isLink = validateLink('helloworld!').isValid; + assert.strictEqual(isLink, false); + }); + + test('Markdown pasting should not occur for a link followed by text.', () => { + const isLink = validateLink('https://www.microsoft.com/ hello world!').isValid; + assert.strictEqual(isLink, false); + }); + + test('Markdown pasting should occur for a link preceded or followed by spaces.', () => { + const isLink = validateLink(' https://www.microsoft.com/ ').isValid; + assert.strictEqual(isLink, true); + }); + + test('Markdown pasting should not occur for a link with an invalid scheme.', () => { + const isLink = validateLink('hello:www.microsoft.com').isValid; + assert.strictEqual(isLink, false); + }); + + test('Markdown pasting should not occur for multiple links being pasted.', () => { + const isLink = validateLink('https://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/').isValid; + assert.strictEqual(isLink, false); + }); + + test('Markdown pasting should not occur for multiple links with spaces being pasted.', () => { + const isLink = validateLink('https://www.microsoft.com/ \r\nhttps://www.microsoft.com/\r\nhttps://www.microsoft.com/\r\n hello \r\nhttps://www.microsoft.com/').isValid; + assert.strictEqual(isLink, false); + }); + }); + + suite('appendToLinkSnippet', () => { + test('Should not create Markdown link snippet when pasteAsMarkdownLink is false', () => { + const uri = vscode.Uri.parse('https://www.microsoft.com/'); + const snippet = appendToLinkSnippet(new vscode.SnippetString(''), false, 'https:/www.microsoft.com', '', uri, 0, true); + assert.strictEqual(snippet?.value, 'https://www.microsoft.com/'); + }); + + test('Should create Markdown link snippet when pasteAsMarkdownLink is true', () => { + const uri = vscode.Uri.parse('https://www.microsoft.com/'); + const snippet = appendToLinkSnippet(new vscode.SnippetString(''), true, 'https:/www.microsoft.com', '', uri, 0, true); + assert.strictEqual(snippet?.value, '[${0:Title}](https://www.microsoft.com/)'); + }); + + test('Should use an unencoded URI string in Markdown link when passing in an external browser link', () => { + const uri = vscode.Uri.parse('https://www.microsoft.com/'); + const snippet = appendToLinkSnippet(new vscode.SnippetString(''), true, 'https:/www.microsoft.com', '', uri, 0, true); + assert.strictEqual(snippet?.value, '[${0:Title}](https://www.microsoft.com/)'); + }); + }); + + + suite('checkSmartPaste', () => { + + const skinnyDocument: SkinnyTextDocument = { + uri: vscode.Uri.file('/path/to/your/file'), + offsetAt: function () { return 0; }, + getText: function () { return 'hello world!'; }, + // lineAt: function (position: vscode.Position) { + // return { + // lineNumber: 0, + // text: 'hello world!', + // range: new vscode.Range(position, position), + // rangeIncludingLineBreak: new vscode.Range(position, position), + // firstNonWhitespaceCharacterIndex: 0, + // isEmptyOrWhitespace: false + // } as vscode.TextLine; + // } + }; + + test('Should evaluate pasteAsMarkdownLink as true for selected plain text', () => { + const range = new vscode.Range(0, 5, 0, 5); + const smartPaste = checkSmartPaste(skinnyDocument, range); + assert.strictEqual(smartPaste.pasteAsMarkdownLink, true); + }); + + test('Should evaluate pasteAsMarkdownLink as false for pasting within a code block', () => { + skinnyDocument.getText = function () { return '```\r\n\r\n```'; }; + const range = new vscode.Range(0, 5, 0, 5); + const smartPaste = checkSmartPaste(skinnyDocument, range); + assert.strictEqual(smartPaste.pasteAsMarkdownLink, false); + }); + + test('Should evaluate pasteAsMarkdownLink as false for pasting within a math block', () => { + skinnyDocument.getText = function () { return '$$$\r\n\r\n$$$'; }; + const range = new vscode.Range(0, 5, 0, 5); + const smartPaste = checkSmartPaste(skinnyDocument, range); + assert.strictEqual(smartPaste.pasteAsMarkdownLink, false); + }); + + const linkSkinnyDoc: SkinnyTextDocument = { + uri: vscode.Uri.file('/path/to/your/file'), + offsetAt: function () { return 0; }, + getText: function () { return '[a](bcdef)'; }, + }; + + test('Should evaluate updateTitle as true for pasting over a Markdown link', () => { + const range = new vscode.Range(0, 0, 0, 10); + const smartPaste = checkSmartPaste(linkSkinnyDoc, range); + assert.strictEqual(smartPaste.updateTitle, true); + }); + + test('Should evaluate pasteAsMarkdownLink as false for pasting within a Markdown link', () => { + const range = new vscode.Range(0, 4, 0, 6); + const smartPaste = checkSmartPaste(linkSkinnyDoc, range); + + assert.strictEqual(smartPaste.pasteAsMarkdownLink, false); + }); + + + const imageLinkSkinnyDoc: SkinnyTextDocument = { + uri: vscode.Uri.file('/path/to/your/file'), + offsetAt: function () { return 0; }, + getText: function () { return '![a](bcdef)'; }, + }; + + test('Should evaluate updateTitle as true for pasting over a Markdown image link', () => { + const range = new vscode.Range(0, 0, 0, 11); + const smartPaste = checkSmartPaste(imageLinkSkinnyDoc, range); + assert.strictEqual(smartPaste.updateTitle, true); + }); + + test('Should evaluate pasteAsMarkdownLink as false for pasting within a Markdown image link', () => { + const range = new vscode.Range(0, 5, 0, 10); + const smartPaste = checkSmartPaste(imageLinkSkinnyDoc, range); + assert.strictEqual(smartPaste.pasteAsMarkdownLink, false); + }); + + const inlineCodeSkinnyCode: SkinnyTextDocument = { + uri: vscode.Uri.file('/path/to/your/file'), + offsetAt: function () { return 0; }, + getText: function () { return '``'; }, + }; + + test('Should evaluate pasteAsMarkdownLink as false for pasting within inline code', () => { + const range = new vscode.Range(0, 1, 0, 1); + const smartPaste = checkSmartPaste(inlineCodeSkinnyCode, range); + assert.strictEqual(smartPaste.pasteAsMarkdownLink, false); + }); + + const inlineMathSkinnyDoc: SkinnyTextDocument = { + uri: vscode.Uri.file('/path/to/your/file'), + offsetAt: function () { return 0; }, + getText: function () { return '$$'; }, + }; + + test('Should evaluate pasteAsMarkdownLink as false for pasting within inline math', () => { + const range = new vscode.Range(0, 1, 0, 1); + const smartPaste = checkSmartPaste(inlineMathSkinnyDoc, range); + assert.strictEqual(smartPaste.pasteAsMarkdownLink, false); + }); + }); +}); diff --git a/extensions/markdown-language-features/src/util/document.ts b/extensions/markdown-language-features/src/util/document.ts index 9c192227ee3b3..856226a737692 100644 --- a/extensions/markdown-language-features/src/util/document.ts +++ b/extensions/markdown-language-features/src/util/document.ts @@ -7,24 +7,24 @@ import * as vscode from 'vscode'; import { Schemes } from './schemes'; import { Utils } from 'vscode-uri'; -export function getDocumentDir(document: vscode.TextDocument): vscode.Uri | undefined { - const docUri = getParentDocumentUri(document); +export function getDocumentDir(uri: vscode.Uri): vscode.Uri | undefined { + const docUri = getParentDocumentUri(uri); if (docUri.scheme === Schemes.untitled) { return vscode.workspace.workspaceFolders?.[0]?.uri; } return Utils.dirname(docUri); } -export function getParentDocumentUri(document: vscode.TextDocument): vscode.Uri { - if (document.uri.scheme === Schemes.notebookCell) { +export function getParentDocumentUri(uri: vscode.Uri): vscode.Uri { + if (uri.scheme === Schemes.notebookCell) { for (const notebook of vscode.workspace.notebookDocuments) { for (const cell of notebook.getCells()) { - if (cell.document === document) { + if (cell.document.uri.toString() === uri.toString()) { return notebook.uri; } } } } - return document.uri; + return uri; } diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index 14fc09e91c1da..646e2779236b9 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -23,10 +23,6 @@ "checkbox.border": "#ffffff1f", "debugToolBar.background": "#181818", "descriptionForeground": "#8b949e", - "diffEditor.insertedLineBackground": "#23863633", - "diffEditor.insertedTextBackground": "#2386364d", - "diffEditor.removedLineBackground": "#da363333", - "diffEditor.removedTextBackground": "#da36334d", "dropdown.background": "#313131", "dropdown.border": "#ffffff1f", "dropdown.foreground": "#cccccc", diff --git a/extensions/theme-defaults/themes/light_modern.json b/extensions/theme-defaults/themes/light_modern.json index 5640a255dc156..c9e486fbab4cb 100644 --- a/extensions/theme-defaults/themes/light_modern.json +++ b/extensions/theme-defaults/themes/light_modern.json @@ -22,10 +22,6 @@ "checkbox.background": "#f8f8f8", "checkbox.border": "#CECECE", "descriptionForeground": "#3b3b3b", - "diffEditor.insertedLineBackground": "#23863633", - "diffEditor.insertedTextBackground": "#2386364d", - "diffEditor.removedLineBackground": "#da363333", - "diffEditor.removedTextBackground": "#da36334d", "dropdown.background": "#ffffff", "dropdown.border": "#CECECE", "dropdown.foreground": "#3b3b3b", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index b2a8bb3a9cc8c..915d85a12b8b5 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, doesNotThrow, equal, ok, strictEqual, throws } from 'assert'; -import { ConfigurationTarget, Disposable, env, EnvironmentVariableCollection, EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableMutatorType, EnvironmentVariableScope, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalExitReason, TerminalOptions, TerminalState, UIKind, Uri, window, workspace } from 'vscode'; +import { commands, ConfigurationTarget, Disposable, env, EnvironmentVariableCollection, EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableMutatorType, EnvironmentVariableScope, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalExitReason, TerminalOptions, TerminalState, UIKind, Uri, window, workspace } from 'vscode'; import { assertNoRpc, poll } from '../utils'; // Disable terminal tests: @@ -347,8 +347,47 @@ import { assertNoRpc, poll } from '../utils'; }); }); + suite('selection', () => { + test('should be undefined immediately after creation', async () => { + const terminal = window.createTerminal({ name: 'selection test' }); + terminal.show(); + equal(terminal.selection, undefined); + terminal.dispose(); + }); + test('should be defined after selecting all content', async () => { + const terminal = window.createTerminal({ name: 'selection test' }); + terminal.show(); + // Wait for some terminal data + await new Promise(r => { + const disposable = window.onDidWriteTerminalData(() => { + disposable.dispose(); + r(); + }); + }); + await commands.executeCommand('workbench.action.terminal.selectAll'); + await poll(() => Promise.resolve(), () => terminal.selection !== undefined, 'selection should be defined'); + terminal.dispose(); + }); + test('should be undefined after clearing a selection', async () => { + const terminal = window.createTerminal({ name: 'selection test' }); + terminal.show(); + // Wait for some terminal data + await new Promise(r => { + const disposable = window.onDidWriteTerminalData(() => { + disposable.dispose(); + r(); + }); + }); + await commands.executeCommand('workbench.action.terminal.selectAll'); + await poll(() => Promise.resolve(), () => terminal.selection !== undefined, 'selection should be defined'); + await commands.executeCommand('workbench.action.terminal.clearSelection'); + await poll(() => Promise.resolve(), () => terminal.selection === undefined, 'selection should not be defined'); + terminal.dispose(); + }); + }); + suite('window.onDidWriteTerminalData', () => { - test('should listen to all future terminal data events', (done) => { + test.skip('should listen to all future terminal data events', (done) => { const openEvents: string[] = []; const dataEvents: { name: string; data: string }[] = []; const closeEvents: string[] = []; diff --git a/package.json b/package.json index a12ce8628838d..4148bf17533be 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.81.0", - "distro": "97db68abc58fce4ef0a70cdaa6255836e6a8d085", + "distro": "912d48f29160a34a4bfe3ebc36b01e5e47433efd", "author": { "name": "Microsoft Corporation" }, @@ -70,7 +70,7 @@ "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", - "@vscode/proxy-agent": "^0.16.0", + "@vscode/proxy-agent": "^0.17.1", "@vscode/ripgrep": "^1.15.5", "@vscode/spdlog": "^0.13.10", "@vscode/sqlite3": "5.1.6-vscode", @@ -83,6 +83,7 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "jschardet": "3.0.0", + "kerberos": "^2.0.1", "keytar": "7.9.0", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", @@ -116,6 +117,7 @@ "@types/gulp-postcss": "^8.0.0", "@types/gulp-svgmin": "^1.2.1", "@types/http-proxy-agent": "^2.0.1", + "@types/kerberos": "^1.1.2", "@types/keytar": "^4.4.0", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", @@ -148,7 +150,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "22.3.14", + "electron": "22.3.17", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^39.3.2", diff --git a/remote/package.json b/remote/package.json index e10bd518df423..c68f4105b30f4 100644 --- a/remote/package.json +++ b/remote/package.json @@ -7,7 +7,7 @@ "@microsoft/1ds-post-js": "^3.2.2", "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.16.0", + "@vscode/proxy-agent": "^0.17.1", "@vscode/ripgrep": "^1.15.5", "@vscode/spdlog": "^0.13.10", "@vscode/vscode-languagedetection": "1.0.21", @@ -18,6 +18,7 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "jschardet": "3.0.0", + "kerberos": "^2.0.1", "keytar": "7.9.0", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", diff --git a/remote/yarn.lock b/remote/yarn.lock index 7c74dbf3e57f9..0034d6a8a56bf 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -58,10 +58,10 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/proxy-agent@^0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.16.0.tgz#32054387f7aaf26d1b5d53f553d53bfd8489eab8" - integrity sha512-b8yBHgdngDrP+9HPJtnPUJjPHd+zfEvOYoc8KioWJVs0rFVT2U77nFDVC70Mrrscf87ya2a/sPY32nTrwFfOQQ== +"@vscode/proxy-agent@^0.17.1": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.17.1.tgz#00ea42fb3565c78c38bc99a73d4460db538aef4e" + integrity sha512-KWQ5y2uB6547Oudx2TMV28PdcdqNzI4J7TZzhZht1kNra8spqOzQJXw6gBdoh2mMFVpNiKgVhZ9YinWR0BZHiw== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -454,6 +454,15 @@ jschardet@3.0.0: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +kerberos@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.0.1.tgz#663b0b46883b4da84495f60f2e9e399a43a33ef5" + integrity sha512-O/jIgbdGK566eUhFwIcgalbqirYU/r76MW7/UFw06Fd9x5bSwgyZWL/Vm26aAmezQww/G9KYkmmJBkEkPk5HLw== + dependencies: + bindings "^1.5.0" + node-addon-api "^4.3.0" + prebuild-install "7.1.1" + keytar@7.9.0: version "7.9.0" resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb" @@ -595,6 +604,24 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +prebuild-install@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prebuild-install@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870" diff --git a/scripts/playground-server.ts b/scripts/playground-server.ts index 9468087409fdd..1c2074ee19134 100644 --- a/scripts/playground-server.ts +++ b/scripts/playground-server.ts @@ -280,7 +280,7 @@ function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): s if (___globalModuleManager._modules2[moduleId]) { const srcUrl = ___globalModuleManager._config.moduleIdToPaths(data.moduleId); const newSrc = await (await fetch(srcUrl)).text(); - (new Function('define', newSrc))(function (deps, callback) { + (new Function('define', newSrc))(function (deps, callback) { // CodeQL [SM01632] This code is only executed during development (as part of the dev-only playground-server). It is required for the hot-reload functionality. const oldModule = ___globalModuleManager._modules2[moduleId]; delete ___globalModuleManager._modules2[moduleId]; diff --git a/src/buildfile.js b/src/buildfile.js index 8917223094ad6..f03de33fb3db5 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -53,7 +53,7 @@ exports.workerProfileAnalysis = [createEditorWorkerModuleDescription('vs/platfor exports.workbenchDesktop = [ createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputer'), - createEditorWorkerModuleDescription('vs/workbench/services/textMate/browser/worker/textMate.worker'), + createEditorWorkerModuleDescription('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'), createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), createModuleDescription('vs/platform/files/node/watcher/watcherMain'), createModuleDescription('vs/platform/terminal/node/ptyHostMain'), @@ -62,7 +62,7 @@ exports.workbenchDesktop = [ exports.workbenchWeb = [ createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputer'), - createEditorWorkerModuleDescription('vs/workbench/services/textMate/browser/worker/textMate.worker'), + createEditorWorkerModuleDescription('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'), createModuleDescription('vs/code/browser/workbench/workbench', ['vs/workbench/workbench.web.main']) ]; diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index 4194d976db257..2ac4671e49786 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -16,7 +16,7 @@ class WindowManager { public getZoomLevel(): number { return this._zoomLevel; } - public setZoomLevel(zoomLevel: number, isTrusted: boolean): void { + public setZoomLevel(zoomLevel: number): void { if (this._zoomLevel === zoomLevel) { return; } @@ -159,8 +159,8 @@ export function addMatchMediaChangeListener(query: string | MediaQueryList, call export const PixelRatio = new PixelRatioFacade(); /** A zoom index, e.g. 1, 2, 3 */ -export function setZoomLevel(zoomLevel: number, isTrusted: boolean): void { - WindowManager.INSTANCE.setZoomLevel(zoomLevel, isTrusted); +export function setZoomLevel(zoomLevel: number): void { + WindowManager.INSTANCE.setZoomLevel(zoomLevel); } export function getZoomLevel(): number { return WindowManager.INSTANCE.getZoomLevel(); diff --git a/src/vs/base/browser/contextmenu.ts b/src/vs/base/browser/contextmenu.ts index 8f67c23030664..3ab23f5bc9ff4 100644 --- a/src/vs/base/browser/contextmenu.ts +++ b/src/vs/base/browser/contextmenu.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview'; +import { AnchorAlignment, AnchorAxisAlignment, IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { IAction, IActionRunner } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { OmitOptional } from 'vs/base/common/types'; export interface IContextMenuEvent { readonly shiftKey?: boolean; @@ -16,8 +18,21 @@ export interface IContextMenuEvent { readonly metaKey?: boolean; } +/** + * A specific context menu location to position the menu at. + * Uses some TypeScript type tricks to prevent allowing to + * pass in a `MouseEvent` and force people to use `StandardMouseEvent`. + */ +type ContextMenuLocation = OmitOptional & { getModifierState?: never }; + export interface IContextMenuDelegate { - getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number }; + /** + * The anchor where to position the context view. + * Use a `HTMLElement` to position the view at the element, + * a `StandardMouseEvent` to position it at the mouse position + * or an `ContextMenuLocation` to position it at a specific location. + */ + getAnchor(): HTMLElement | StandardMouseEvent | ContextMenuLocation; getActions(): readonly IAction[]; getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox'; getActionViewItem?(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 04931cca3f09d..7f94015706bca 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -5,9 +5,11 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import * as DOM from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { Range } from 'vs/base/common/range'; +import { OmitOptional } from 'vs/base/common/types'; import 'vs/css!./contextview'; export const enum ContextViewDOMPosition { @@ -23,6 +25,12 @@ export interface IAnchor { height?: number; } +export function isAnchor(obj: unknown): obj is IAnchor | OmitOptional { + const anchor = obj as IAnchor | OmitOptional | undefined; + + return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number'; +} + export const enum AnchorAlignment { LEFT, RIGHT } @@ -36,7 +44,13 @@ export const enum AnchorAxisAlignment { } export interface IDelegate { - getAnchor(): HTMLElement | IAnchor; + /** + * The anchor where to position the context view. + * Use a `HTMLElement` to position the view at the element, + * a `StandardMouseEvent` to position it at the mouse position + * or an `IAnchor` to position it at a specific location. + */ + getAnchor(): HTMLElement | StandardMouseEvent | IAnchor; render(container: HTMLElement): IDisposable | null; focus?(): void; layout?(): void; @@ -271,13 +285,20 @@ export class ContextView extends Disposable { width: elementPosition.width * zoom, height: elementPosition.height * zoom }; - } else { + } else if (isAnchor(anchor)) { around = { top: anchor.y, left: anchor.x, width: anchor.width || 1, height: anchor.height || 2 }; + } else { + around = { + top: anchor.posy, + left: anchor.posx, + width: 1, + height: 2 + }; } const viewSizeWidth = DOM.getTotalWidth(this.view); diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index aee3cad23012e..2562b2fa153e4 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -455,7 +455,8 @@ export class Grid extends Disposable { if (sizing?.type === 'distribute') { gridViewSizing = GridViewSizing.Distribute; } else if (sizing?.type === 'auto') { - gridViewSizing = GridViewSizing.Auto(0); + const index = location[location.length - 1]; + gridViewSizing = GridViewSizing.Auto(index === 0 ? 1 : index - 1); } this.gridview.removeView(location, gridViewSizing); diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index 0ce581993549b..f008173e71685 100644 --- a/src/vs/base/browser/ui/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -27,7 +27,7 @@ } .monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) { - max-width: 500px; + max-width: var(--hover-maxWidth, 500px); word-wrap: break-word; } diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index f776ba7ecb2ea..2a77409cd830f 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -5,6 +5,7 @@ import { IDragAndDropData } from 'vs/base/browser/dnd'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { GestureEvent } from 'vs/base/browser/touch'; export interface IListVirtualDelegate { @@ -61,7 +62,7 @@ export interface IListContextMenuEvent { readonly browserEvent: UIEvent; readonly element: T | undefined; readonly index: number | undefined; - readonly anchor: HTMLElement | { readonly x: number; readonly y: number }; + readonly anchor: HTMLElement | IMouseEvent; } export interface IIdentityProvider { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index e8ec5372e18f5..36c5a74a94816 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -27,6 +27,7 @@ import { isNumber } from 'vs/base/common/types'; 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, ListView } from './listView'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; interface ITraitChangeEvent { indexes: number[]; @@ -1354,7 +1355,7 @@ export class List implements ISpliceable, IDisposable { const fromMouse = this.disposables.add(Event.chain(this.view.onContextMenu)) .filter(_ => !didJustPressContextMenuKey) - .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY }, browserEvent })) + .map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(browserEvent), browserEvent })) .event; return Event.any>(fromKeyDown, fromKeyUp, fromMouse); diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index a4edaa6a3d900..b822db43751e5 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -259,7 +259,7 @@ abstract class ViewItem { constructor( protected container: HTMLElement, - private view: IView, + readonly view: IView, size: ViewItemSize, private disposable: IDisposable ) { @@ -280,9 +280,8 @@ abstract class ViewItem { abstract layoutContainer(offset: number): void; - dispose(): IView { + dispose(): void { this.disposable.dispose(); - return this.view; } } @@ -662,13 +661,20 @@ export class SplitView extends Disposable { if (this.areViewsDistributed()) { sizing = { type: 'distribute' }; } else { - sizing = undefined; + sizing = { type: 'split', index: sizing.index }; } } + // Save referene view, in case of `split` sizing + const referenceViewItem = sizing?.type === 'split' ? this.viewItems[sizing.index] : undefined; + // Remove view - const viewItem = this.viewItems.splice(index, 1)[0]; - const view = viewItem.dispose(); + const viewItemToRemove = this.viewItems.splice(index, 1)[0]; + + // Resize reference view, in case of `split` sizing + if (referenceViewItem) { + referenceViewItem.size += viewItemToRemove.size; + } // Remove sash if (this.viewItems.length >= 1) { @@ -684,7 +690,9 @@ export class SplitView extends Disposable { this.distributeViewSizes(); } - return view; + const result = viewItemToRemove.view; + viewItemToRemove.dispose(); + return result; } /** diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 94e8650f2defa..262badedf9fd2 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDragAndDropData } from 'vs/base/browser/dnd'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IListDragAndDrop, IListDragOverReaction, IListRenderer, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; import { Event } from 'vs/base/common/event'; @@ -176,7 +177,7 @@ export interface ITreeMouseEvent { export interface ITreeContextMenuEvent { readonly browserEvent: UIEvent; readonly element: T | null; - readonly anchor: HTMLElement | { readonly x: number; readonly y: number }; + readonly anchor: HTMLElement | IMouseEvent; } export interface ITreeNavigator { diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 1633395798901..897a9fd824971 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -230,10 +230,9 @@ export function filter(obj: obj, predicate: (key: string, value: any) => boolean export function getAllPropertyNames(obj: object): string[] { let res: string[] = []; - let proto = Object.getPrototypeOf(obj); - while (Object.prototype !== proto) { - res = res.concat(Object.getOwnPropertyNames(proto)); - proto = Object.getPrototypeOf(proto); + while (Object.prototype !== obj) { + res = res.concat(Object.getOwnPropertyNames(obj)); + obj = Object.getPrototypeOf(obj); } return res; } diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index c35da047d1f6f..c66ad01809794 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -225,6 +225,10 @@ export type AddFirstParameterToFunctions }> = Partial & U[keyof U]; +/** + * Only picks the non-optional properties of a type. + */ +export type OmitOptional = { [K in keyof T as T[K] extends Required[K] ? K : never]: T[K] }; /** * A type that removed readonly-less from all properties of `T` diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index d71baaeeeb571..747e8513b1f01 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -8,7 +8,7 @@ import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/com import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; -import { CancellationError } from 'vs/base/common/errors'; +import { CancellationError, ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event, EventMultiplexer, Relay } from 'vs/base/common/event'; import { combinedDisposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; @@ -1102,7 +1102,7 @@ export namespace ProxyChannel { } } - throw new Error(`Event not found: ${event}`); + throw new ErrorNoTelemetry(`Event not found: ${event}`); } call(_: unknown, command: string, args?: any[]): Promise { @@ -1119,7 +1119,7 @@ export namespace ProxyChannel { return target.apply(handler, args); } - throw new Error(`Method not found: ${command}`); + throw new ErrorNoTelemetry(`Method not found: ${command}`); } }; } @@ -1185,7 +1185,7 @@ export namespace ProxyChannel { }; } - throw new Error(`Property not found: ${String(propKey)}`); + throw new ErrorNoTelemetry(`Property not found: ${String(propKey)}`); } }) as T; } diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index a41b5dbf7bdc6..addc28f0c4d37 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -43,7 +43,9 @@ // Set up nls if the user is not using the default language (English) const nlsConfig = {}; - const locale = window.localStorage.getItem('vscode.nls.locale') || navigator.language; + // Normalize locale to lowercase because translationServiceUrl is case-sensitive. + // ref: https://github.com/microsoft/vscode/issues/187795 + const locale = window.localStorage.getItem('vscode.nls.locale') || navigator.language.toLowerCase(); if (!locale.startsWith('en')) { nlsConfig['vs/nls'] = { availableLanguages: { diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index bc95af4fe9e61..94fa1a8bdb173 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -35,6 +35,7 @@ import { TokenizationRegistry } from 'vs/editor/common/languages'; import { ColorId, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes'; import { Color } from 'vs/base/common/color'; import { IME } from 'vs/base/common/ime'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export interface IVisibleRangeProvider { visibleRangeForPosition(position: Position): HorizontalPosition | null; @@ -140,7 +141,12 @@ export class TextAreaHandler extends ViewPart { public readonly textAreaCover: FastDomNode; private readonly _textAreaInput: TextAreaInput; - constructor(context: ViewContext, viewController: ViewController, visibleRangeProvider: IVisibleRangeProvider) { + constructor( + context: ViewContext, + viewController: ViewController, + visibleRangeProvider: IVisibleRangeProvider, + @IKeybindingService private readonly _keybindingService: IKeybindingService + ) { super(context); this._viewController = viewController; @@ -553,7 +559,21 @@ export class TextAreaHandler extends ViewPart { private _getAriaLabel(options: IComputedEditorOptions): string { const accessibilitySupport = options.get(EditorOption.accessibilitySupport); if (accessibilitySupport === AccessibilitySupport.Disabled) { - return nls.localize('accessibilityOffAriaLabel', "The editor is not accessible at this time. Press {0} for options.", platform.isLinux ? 'Shift+Alt+F1' : 'Alt+F1'); + + const toggleKeybindingLabel = this._keybindingService.lookupKeybinding('editor.action.toggleScreenReaderAccessibilityMode')?.getAriaLabel(); + const runCommandKeybindingLabel = this._keybindingService.lookupKeybinding('workbench.action.showCommands')?.getAriaLabel(); + const keybindingEditorKeybindingLabel = this._keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings')?.getAriaLabel(); + const editorNotAccessibleMessage = nls.localize('accessibilityModeOff', "The editor is not accessible at this time."); + if (toggleKeybindingLabel) { + return nls.localize('accessibilityOffAriaLabel', "{0} To enable screen reader optimized mode, use {1}", editorNotAccessibleMessage, toggleKeybindingLabel); + } else if (runCommandKeybindingLabel) { + return nls.localize('accessibilityOffAriaLabelNoKb', "{0} To enable screen reader optimized mode, open the quick pick with {1} and run the command Toggle Screen Reader Accessibility Mode, which is currently not triggerable via keyboard.", editorNotAccessibleMessage, runCommandKeybindingLabel); + } else if (keybindingEditorKeybindingLabel) { + return nls.localize('accessibilityOffAriaLabelNoKbs', "{0} Please assign a keybinding for the command Toggle Screen Reader Accessibility Mode by accessing the keybindings editor with {1} and run it.", editorNotAccessibleMessage, keybindingEditorKeybindingLabel); + } else { + // SOS + return editorNotAccessibleMessage; + } } return options.get(EditorOption.ariaLabel); } diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 4d6c57410be56..d037337887ccc 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1263,9 +1263,9 @@ export interface IDiffEditor extends editorCommon.IEditor { */ revealFirstDiff(): unknown; - diffReviewNext(): void; + accessibleDiffViewerNext(): void; - diffReviewPrev(): void; + accessibleDiffViewerPrev(): void; } /** diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 82d1fb7a26969..0e791b1430570 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -54,6 +54,7 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { WhitespaceOverlay } from 'vs/editor/browser/viewParts/whitespace/whitespace'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; import { GlyphMarginLane } from 'vs/editor/common/model'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export interface IContentWidgetData { @@ -106,7 +107,8 @@ export class View extends ViewEventHandler { colorTheme: IColorTheme, model: IViewModel, userInputEvents: ViewUserInputEvents, - overflowWidgetsDomNode: HTMLElement | undefined + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._selections = [new Selection(1, 1, 1, 1)]; @@ -123,7 +125,7 @@ export class View extends ViewEventHandler { this._viewParts = []; // Keyboard handler - this._textAreaHandler = new TextAreaHandler(this._context, viewController, this._createTextAreaHandlerHelper()); + this._textAreaHandler = this._instantiationService.createInstance(TextAreaHandler, this._context, viewController, this._createTextAreaHandlerHelper()); this._viewParts.push(this._textAreaHandler); // These two dom nodes must be constructed up front, since references are needed in the layout provider (scrolling & co.) diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index c05cb46f268b9..f6389bf5410f0 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1853,7 +1853,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._themeService.getColorTheme(), viewModel, viewUserInputEvents, - this._overflowWidgetsDomNode + this._overflowWidgetsDomNode, + this._instantiationService ); return [view, true]; diff --git a/src/vs/editor/browser/widget/diffEditor.contribution.ts b/src/vs/editor/browser/widget/diffEditor.contribution.ts index 456dc90b2930a..c7f1f9d5398c0 100644 --- a/src/vs/editor/browser/widget/diffEditor.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditor.contribution.ts @@ -5,59 +5,78 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +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 } from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; +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 { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -export class DiffReviewNext extends EditorAction { - public static id = 'editor.action.diffReview.next'; +const accessibleDiffViewerCategory: ILocalizedString = { + value: localize('accessibleDiffViewer', 'Accessible Diff Viewer'), + original: 'Accessible Diff Viewer', +}; + +export class AccessibleDiffViewerNext extends EditorAction2 { + public static id = 'editor.action.accessibleDiffViewer.next'; constructor() { super({ - id: DiffReviewNext.id, - label: localize('editor.action.diffReview.next', "Go to Next Difference"), - alias: 'Go to Next Difference', + id: AccessibleDiffViewerNext.id, + title: { value: localize('editor.action.accessibleDiffViewer.next', "Go to Next Difference"), original: 'Go to Next Difference' }, + category: accessibleDiffViewerCategory, precondition: ContextKeyExpr.has('isInDiffEditor'), - kbOpts: { - kbExpr: null, + keybinding: { primary: KeyCode.F7, weight: KeybindingWeight.EditorContrib - } + }, + f1: true, }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { const diffEditor = findFocusedDiffEditor(accessor); - diffEditor?.diffReviewNext(); + diffEditor?.accessibleDiffViewerNext(); } } -export class DiffReviewPrev extends EditorAction { - public static id = 'editor.action.diffReview.prev'; +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: AccessibleDiffViewerNext.id, + title: localize('Open Accessible Diff Viewer', "Open Accessible Diff Viewer"), + }, + order: 10, + group: '2_diff', + when: EditorContextKeys.accessibleDiffViewerVisible.negate(), +}); + +export class AccessibleDiffViewerPrev extends EditorAction2 { + public static id = 'editor.action.accessibleDiffViewer.prev'; constructor() { super({ - id: DiffReviewPrev.id, - label: localize('editor.action.diffReview.prev', "Go to Previous Difference"), - alias: 'Go to Previous Difference', + id: AccessibleDiffViewerPrev.id, + title: { value: localize('editor.action.accessibleDiffViewer.prev', "Go to Previous Difference"), original: 'Go to Previous Difference' }, + category: accessibleDiffViewerCategory, precondition: ContextKeyExpr.has('isInDiffEditor'), - kbOpts: { - kbExpr: null, + keybinding: { primary: KeyMod.Shift | KeyCode.F7, weight: KeybindingWeight.EditorContrib - } + }, + f1: true, }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { const diffEditor = findFocusedDiffEditor(accessor); - diffEditor?.diffReviewPrev(); + diffEditor?.accessibleDiffViewerPrev(); } } -function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { +export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { const codeEditorService = accessor.get(ICodeEditorService); const diffEditors = codeEditorService.listDiffEditors(); const activeCodeEditor = codeEditorService.getFocusedCodeEditor() ?? codeEditorService.getActiveCodeEditor(); @@ -74,5 +93,8 @@ function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { return null; } -registerEditorAction(DiffReviewNext); -registerEditorAction(DiffReviewPrev); +CommandsRegistry.registerCommandAlias('editor.action.diffReview.next', AccessibleDiffViewerNext.id); +registerAction2(AccessibleDiffViewerNext); + +CommandsRegistry.registerCommandAlias('editor.action.diffReview.prev', AccessibleDiffViewerPrev.id); +registerAction2(AccessibleDiffViewerPrev); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index e21c1e9f3beb2..a8ac9eb70b580 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -299,6 +299,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE collapseUnchangedRegions: false, }, isInEmbeddedEditor: false, + onlyShowAccessibleDiffViewer: false, }); this.isEmbeddedDiffEditorKey = EditorContextKeys.isEmbeddedDiffEditor.bindTo(this._contextKeyService); @@ -444,11 +445,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return dom.isAncestor(document.activeElement, this._domElement); } - public diffReviewNext(): void { + public accessibleDiffViewerNext(): void { this._reviewPane.next(); } - public diffReviewPrev(): void { + public accessibleDiffViewerPrev(): void { this._reviewPane.prev(); } @@ -1368,7 +1369,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._originalDomNode.style.width = splitPoint + 'px'; this._originalDomNode.style.left = '0px'; - this._modifiedDomNode.style.width = (width - splitPoint) + 'px'; + this._modifiedDomNode.style.width = (width - splitPoint - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH) + 'px'; this._modifiedDomNode.style.left = splitPoint + 'px'; this._overviewDomElement.style.top = '0px'; @@ -2743,6 +2744,7 @@ function validateDiffEditorOptions(options: Readonly, defaul collapseUnchangedRegions: false, }, isInEmbeddedEditor: validateBooleanOption(options.isInEmbeddedEditor, defaults.isInEmbeddedEditor), + onlyShowAccessibleDiffViewer: false, }; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts index 346f847085edf..bb5aaca0e60c6 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts @@ -10,7 +10,7 @@ import { Action } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, ISettableObservable, ITransaction, autorun, constObservable, derived, keepAlive, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, ITransaction, autorun, derived, keepAlive, observableValue, transaction } from 'vs/base/common/observable'; import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun'; import { subtransaction } from 'vs/base/common/observableImpl/base'; import { derivedWithStore } from 'vs/base/common/observableImpl/derived'; @@ -35,14 +35,16 @@ import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioC import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -const diffReviewInsertIcon = registerIcon('diff-review-insert', Codicon.add, localize('diffReviewInsertIcon', 'Icon for \'Insert\' in diff review.')); -const diffReviewRemoveIcon = registerIcon('diff-review-remove', Codicon.remove, localize('diffReviewRemoveIcon', 'Icon for \'Remove\' in diff review.')); -const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, localize('diffReviewCloseIcon', 'Icon for \'Close\' in diff review.')); +const accessibleDiffViewerInsertIcon = registerIcon('diff-review-insert', Codicon.add, localize('accessibleDiffViewerInsertIcon', 'Icon for \'Insert\' in accessible diff viewer.')); +const accessibleDiffViewerRemoveIcon = registerIcon('diff-review-remove', Codicon.remove, localize('accessibleDiffViewerRemoveIcon', 'Icon for \'Remove\' in accessible diff viewer.')); +const accessibleDiffViewerCloseIcon = registerIcon('diff-review-close', Codicon.close, localize('accessibleDiffViewerCloseIcon', 'Icon for \'Close\' in accessible diff viewer.')); export class AccessibleDiffViewer extends Disposable { constructor( private readonly _parentNode: HTMLElement, - private readonly _visible: ISettableObservable, + private readonly _visible: IObservable, + private readonly _setVisible: (visible: boolean, tx: ITransaction | undefined) => void, + private readonly _canClose: IObservable, private readonly _width: IObservable, private readonly _height: IObservable, private readonly _diffs: IObservable, @@ -59,7 +61,7 @@ export class AccessibleDiffViewer extends Disposable { if (!visible) { return null; } - const model = store.add(this._instantiationService.createInstance(ViewModel, this._diffs, this._editors, this._visible)); + const model = store.add(this._instantiationService.createInstance(ViewModel, this._diffs, this._editors, this._setVisible, this._canClose)); const view = store.add(this._instantiationService.createInstance(View, this._parentNode, model, this._width, this._height, this._editors)); return { model, @@ -70,7 +72,7 @@ export class AccessibleDiffViewer extends Disposable { next(): void { transaction(tx => { const isVisible = this._visible.get(); - this._visible.set(true, tx); + this._setVisible(true, tx); if (isVisible) { this.model.get()!.model.nextGroup(tx); } @@ -79,14 +81,14 @@ export class AccessibleDiffViewer extends Disposable { prev(): void { transaction(tx => { - this._visible.set(true, tx); + this._setVisible(true, tx); this.model.get()!.model.previousGroup(tx); }); } close(): void { transaction(tx => { - this._visible.set(false, tx); + this._setVisible(false, tx); }); } } @@ -104,12 +106,11 @@ class ViewModel extends Disposable { public readonly currentElement: IObservable = this._currentElementIdx.map((idx, r) => this.currentGroup.read(r)?.lines[idx]); - public readonly canClose: IObservable = constObservable(true); - constructor( private readonly _diffs: IObservable, private readonly _editors: DiffEditorEditors, - private readonly _visible: ISettableObservable, + private readonly _setVisible: (visible: boolean, tx: ITransaction | undefined) => void, + public readonly canClose: IObservable, @IAudioCueService private readonly _audioCueService: IAudioCueService, ) { super(); @@ -192,7 +193,7 @@ class ViewModel extends Disposable { } revealCurrentElementInEditor(): void { - this._visible.set(false, undefined); + this._setVisible(false, undefined); const curElem = this.currentElement.get(); if (curElem) { @@ -211,7 +212,7 @@ class ViewModel extends Disposable { } close(): void { - this._visible.set(false, undefined); + this._setVisible(false, undefined); this._editors.modified.focus(); } } @@ -338,14 +339,18 @@ class View extends Disposable { this._actionBar = this._register(new ActionBar( actionBarContainer )); - this._actionBar.push(new Action( - 'diffreview.close', - localize('label.close', "Close"), - 'close-diff-review ' + ThemeIcon.asClassName(diffReviewCloseIcon), - true, - async () => _model.close() - ), { label: false, icon: true }); - + this._register(autorun('update actions', reader => { + this._actionBar.clear(); + if (this._model.canClose.read(reader)) { + this._actionBar.push(new Action( + 'diffreview.close', + localize('label.close', "Close"), + 'close-diff-review ' + ThemeIcon.asClassName(accessibleDiffViewerCloseIcon), + true, + async () => _model.close() + ), { label: false, icon: true }); + } + })); this._content = document.createElement('div'); this._content.className = 'diff-review-content'; @@ -521,12 +526,12 @@ class View extends Disposable { case LineType.Added: rowClassName = 'diff-review-row line-insert'; lineNumbersExtraClassName = ' char-insert'; - spacerIcon = diffReviewInsertIcon; + spacerIcon = accessibleDiffViewerInsertIcon; break; case LineType.Deleted: rowClassName = 'diff-review-row line-delete'; lineNumbersExtraClassName = ' char-delete'; - spacerIcon = diffReviewRemoveIcon; + spacerIcon = accessibleDiffViewerRemoveIcon; break; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts index 645205e061f81..34ee3e06cdf3d 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts @@ -159,6 +159,6 @@ export class DiffEditorEditors extends Disposable { } else if (ariaLabel) { return ariaLabel.replaceAll(ariaNavigationTip, ''); } - return undefined; + return ''; } } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts index fcec4e86f0926..eed16b994722a 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts @@ -46,6 +46,7 @@ export class DiffEditorOptions { public readonly accessibilityVerbose = derived('accessibilityVerbose', reader => this._options.read(reader).accessibilityVerbose); public readonly diffAlgorithm = derived('diffAlgorithm', reader => this._options.read(reader).diffAlgorithm); public readonly showEmptyDecorations = derived('showEmptyDecorations', reader => this._options.read(reader).experimental.showEmptyDecorations!); + public readonly onlyShowAccessibleDiffViewer = derived('onlyShowAccessibleDiffViewer', reader => this._options.read(reader).onlyShowAccessibleDiffViewer); public updateOptions(changedOptions: IDiffEditorOptions): void { const newDiffEditorOptions = validateDiffEditorOptions(changedOptions, this._options.get()); @@ -75,6 +76,7 @@ const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = { showEmptyDecorations: true, }, isInEmbeddedEditor: false, + onlyShowAccessibleDiffViewer: false, }; function validateDiffEditorOptions(options: Readonly, defaults: ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions { @@ -99,5 +101,6 @@ function validateDiffEditorOptions(options: Readonly, defaul showEmptyDecorations: validateBooleanOption(options.experimental?.showEmptyDecorations, defaults.experimental.showEmptyDecorations!), }, isInEmbeddedEditor: validateBooleanOption(options.isInEmbeddedEditor, defaults.isInEmbeddedEditor), + onlyShowAccessibleDiffViewer: validateBooleanOption(options.onlyShowAccessibleDiffViewer, defaults.onlyShowAccessibleDiffViewer), }; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts index 11543c2c18db2..a8ef3758feed4 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution.ts @@ -5,12 +5,15 @@ import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { findFocusedDiffEditor } from 'vs/editor/browser/widget/diffEditor.contribution'; +import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; import { localize } from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import './colors'; export class ToggleCollapseUnchangedRegions extends Action2 { constructor() { @@ -77,6 +80,10 @@ export class ToggleShowMovedCodeBlocks extends Action2 { registerAction2(ToggleShowMovedCodeBlocks); +/* +TODO@hediet add this back once move detection is more polished. +Users can still enable this via settings.json (config.diffEditor.experimental.showMoves). + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: new ToggleShowMovedCodeBlocks().desc.id, @@ -88,3 +95,30 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { group: '1_diff', when: ContextKeyEqualsExpr.create('diffEditorVersion', 2) }); +*/ + +const diffEditorCategory: ILocalizedString = { + value: localize('diffEditor', 'Diff Editor'), + original: 'Diff Editor', +}; +export class SwitchSide extends EditorAction2 { + constructor() { + super({ + id: 'diffEditor.switchSide', + title: { value: localize('switchSide', "Switch Side"), original: 'Switch Side' }, + icon: Codicon.arrowSwap, + precondition: ContextKeyExpr.and(ContextKeyEqualsExpr.create('diffEditorVersion', 2), ContextKeyExpr.has('isInDiffEditor')), + f1: true, + category: diffEditorCategory, + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): void { + const diffEditor = findFocusedDiffEditor(accessor); + if (diffEditor instanceof DiffEditorWidget2) { + diffEditor.switchSide(); + } + } +} + +registerAction2(SwitchSide); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts index 82c59dd4ddcc0..aff313ade82c5 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts @@ -42,6 +42,11 @@ import { DiffEditorEditors } from './diffEditorEditors'; import { DiffEditorOptions } from './diffEditorOptions'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel'; import { AccessibleDiffViewer } from 'vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer'; +import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; +import { LengthObj } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { Range } from 'vs/editor/common/core/range'; +import './colors'; export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [ @@ -66,7 +71,12 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { private unchangedRangesFeature!: UnchangedRangesFeature; - private _accessibleDiffViewerVisible = observableValue('accessibleDiffViewerVisible', false); + private _accessibleDiffViewerShouldBeVisible = observableValue('accessibleDiffViewerShouldBeVisible', false); + private _accessibleDiffViewerVisible = derived('accessibleDiffViewerVisible', reader => + this._options.onlyShowAccessibleDiffViewer.read(reader) + ? true + : this._accessibleDiffViewerShouldBeVisible.read(reader) + ); private _accessibleDiffViewer!: AccessibleDiffViewer; private readonly _options: DiffEditorOptions; private readonly _editors: DiffEditorEditors; @@ -78,6 +88,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { @IContextKeyService private readonly _parentContextKeyService: IContextKeyService, @IInstantiationService private readonly _parentInstantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, + @IAudioCueService private readonly _audioCueService: IAudioCueService, ) { super(); codeEditorService.willCreateDiffEditor(); @@ -93,6 +104,11 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { isEmbeddedDiffEditorKey.set(this._options.isInEmbeddedEditor.read(reader)); })); + const accessibleDiffViewerVisibleContextKeyValue = EditorContextKeys.accessibleDiffViewerVisible.bindTo(this._contextKeyService); + this._register(autorun('update accessibleDiffViewerVisible context key', reader => { + accessibleDiffViewerVisibleContextKeyValue.set(this._accessibleDiffViewerVisible.read(reader)); + })); + this._domElement.appendChild(this.elements.root); this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension)); @@ -163,6 +179,8 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { readHotReloadableExport(AccessibleDiffViewer, reader), this.elements.accessibleDiffViewer, this._accessibleDiffViewerVisible, + (visible, tx) => this._accessibleDiffViewerShouldBeVisible.set(visible, tx), + this._options.onlyShowAccessibleDiffViewer.map(v => !v), this._rootSizeObserver.width, this._rootSizeObserver.height, this._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)), @@ -228,6 +246,19 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { event.event.stopPropagation(); } })); + + 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.modifiedRange.contains(e.position.lineNumber)); + if (diff?.lineRangeMapping.modifiedRange.isEmpty) { + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + } else if (diff?.lineRangeMapping.originalRange.isEmpty) { + this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + } else if (diff) { + this._audioCueService.playAudioCue(AudioCue.diffLineModified); + } + } + })); } public getContentHeight() { @@ -245,19 +276,16 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { const sashLeft = this._sash.read(reader)?.sashLeft.read(reader); const originalWidth = sashLeft ?? Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft); + const modifiedWidth = width - originalWidth - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); this.elements.original.style.width = originalWidth + 'px'; this.elements.original.style.left = '0px'; - this.elements.modified.style.width = (width - originalWidth) + 'px'; + this.elements.modified.style.width = modifiedWidth + 'px'; this.elements.modified.style.left = originalWidth + 'px'; - this._editors.original.layout({ width: originalWidth, height: height }); - this._editors.modified.layout({ - width: width - originalWidth - - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), - height - }); + this._editors.original.layout({ width: originalWidth, height }); + this._editors.modified.layout({ width: modifiedWidth, height }); return { modifiedEditor: this._editors.modified.getLayoutInfo(), @@ -425,6 +453,14 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { diff = findLast(diffs, d => d.lineRangeMapping.modifiedRange.startLineNumber < curLineNumber) ?? diffs[diffs.length - 1]; } this._goTo(diff); + + if (diff.lineRangeMapping.modifiedRange.isEmpty) { + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + } else if (diff.lineRangeMapping.originalRange.isEmpty) { + this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + } else if (diff) { + this._audioCueService.playAudioCue(AudioCue.diffLineModified); + } } revealFirstDiff(): void { @@ -442,15 +478,80 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { }); } - diffReviewNext(): void { this._accessibleDiffViewer.next(); } + accessibleDiffViewerNext(): void { this._accessibleDiffViewer.next(); } - diffReviewPrev(): void { this._accessibleDiffViewer.prev(); } + accessibleDiffViewerPrev(): void { this._accessibleDiffViewer.prev(); } async waitForDiff(): Promise { const diffModel = this._diffModel.get(); if (!diffModel) { return; } await diffModel.waitForDiff(); } + + switchSide(): void { + const isModifiedFocus = this._editors.modified.hasWidgetFocus(); + const source = isModifiedFocus ? this._editors.modified : this._editors.original; + const destination = isModifiedFocus ? this._editors.original : this._editors.modified; + + const sourceSelection = source.getSelection(); + if (sourceSelection) { + const mappings = this._diffModel.get()?.diff.get()?.mappings.map(m => isModifiedFocus ? m.lineRangeMapping.flip() : m.lineRangeMapping); + if (mappings) { + const newRange1 = translatePosition(sourceSelection.getStartPosition(), mappings); + const newRange2 = translatePosition(sourceSelection.getEndPosition(), mappings); + const range = Range.plusRange(newRange1, newRange2); + destination.setSelection(range); + } + } + destination.focus(); + } +} + +function translatePosition(posInOriginal: Position, mappings: LineRangeMapping[]): Range { + const mapping = findLast(mappings, m => m.originalRange.startLineNumber <= posInOriginal.lineNumber); + if (!mapping) { + // No changes before the position + return Range.fromPositions(posInOriginal); + } + + if (mapping.originalRange.endLineNumberExclusive <= posInOriginal.lineNumber) { + const newLineNumber = posInOriginal.lineNumber - mapping.originalRange.endLineNumberExclusive + mapping.modifiedRange.endLineNumberExclusive; + return Range.fromPositions(new Position(newLineNumber, posInOriginal.column)); + } + + if (!mapping.innerChanges) { + // Only for legacy algorithm + return Range.fromPositions(new Position(mapping.modifiedRange.startLineNumber, 1)); + } + + const innerMapping = findLast(mapping.innerChanges, m => m.originalRange.getStartPosition().isBeforeOrEqual(posInOriginal)); + if (!innerMapping) { + const newLineNumber = posInOriginal.lineNumber - mapping.originalRange.startLineNumber + mapping.modifiedRange.startLineNumber; + return Range.fromPositions(new Position(newLineNumber, posInOriginal.column)); + } + + if (innerMapping.originalRange.containsPosition(posInOriginal)) { + return innerMapping.modifiedRange; + } else { + const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal); + return Range.fromPositions(addLength(innerMapping.modifiedRange.getEndPosition(), l)); + } +} + +function lengthBetweenPositions(position1: Position, position2: Position): LengthObj { + if (position1.lineNumber === position2.lineNumber) { + return new LengthObj(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); + } } function toLineChanges(state: DiffState): ILineChange[] { diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts index 9960732a03faf..a3c29b25b4ebc 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts @@ -85,7 +85,9 @@ export class ViewZoneManager extends Disposable { const diff = diffModel?.diff.read(reader); if (!diffModel || !diff) { return null; } state.read(reader); - return computeRangeAlignment(this._editors.original, this._editors.modified, diff.mappings, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod); + const renderSideBySide = this._options.renderSideBySide.read(reader); + const innerHunkAlignment = renderSideBySide; + return computeRangeAlignment(this._editors.original, this._editors.modified, diff.mappings, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod, innerHunkAlignment); }); const alignmentsSyncedMovedText = derived('alignments', (reader) => { @@ -94,7 +96,7 @@ export class ViewZoneManager extends Disposable { state.read(reader); const mappings = syncedMovedText.changes.map(c => new DiffMapping(c)); // TODO dont include alignments outside syncedMovedText - return computeRangeAlignment(this._editors.original, this._editors.modified, mappings, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod); + return computeRangeAlignment(this._editors.original, this._editors.modified, mappings, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod, true); }); function createFakeLinesDiv(): HTMLElement { @@ -455,6 +457,7 @@ function computeRangeAlignment( diffs: readonly DiffMapping[], originalEditorAlignmentViewZones: ReadonlySet, modifiedEditorAlignmentViewZones: ReadonlySet, + innerHunkAlignment: boolean, ): ILineRangeAlignment[] { const originalLineHeightOverrides = new ArrayQueue(getAdditionalLineHeights(originalEditor, originalEditorAlignmentViewZones)); const modifiedLineHeightOverrides = new ArrayQueue(getAdditionalLineHeights(modifiedEditor, modifiedEditorAlignmentViewZones)); @@ -546,18 +549,24 @@ function computeRangeAlignment( modifiedRange, originalHeightInPx: originalRange.length * origLineHeight + originalAdditionalHeight, modifiedHeightInPx: modifiedRange.length * modLineHeight + modifiedAdditionalHeight, + diff: m.lineRangeMapping, }); lastOrigLineNumber = origLineNumberExclusive; lastModLineNumber = modLineNumberExclusive; } - for (const i of c.innerChanges || []) { - if (i.originalRange.startColumn > 1 && i.modifiedRange.startColumn > 1) { - // There is some unmodified text on this line - emitAlignment(i.originalRange.startLineNumber, i.modifiedRange.startLineNumber); + if (innerHunkAlignment) { + for (const i of c.innerChanges || []) { + if (i.originalRange.startColumn > 1 && i.modifiedRange.startColumn > 1) { + // There is some unmodified text on this line before the diff + emitAlignment(i.originalRange.startLineNumber, i.modifiedRange.startLineNumber); + } + if (i.originalRange.endColumn < originalEditor.getModel()!.getLineMaxColumn(i.originalRange.endLineNumber)) { + // // There is some unmodified text on this line after the diff + emitAlignment(i.originalRange.endLineNumber, i.modifiedRange.endLineNumber); + } } - emitAlignment(i.originalRange.endLineNumber, i.modifiedRange.endLineNumber); } emitAlignment(c.originalRange.endLineNumberExclusive, c.modifiedRange.endLineNumberExclusive); diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 06c449a33eaa0..dc5dd6c299cce 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -21,6 +21,7 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; +import { IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; export class EmbeddedCodeEditorWidget extends CodeEditorWidget { @@ -132,8 +133,9 @@ export class EmbeddedDiffEditorWidget2 extends DiffEditorWidget2 { @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, + @IAudioCueService audioCueService: IAudioCueService, ) { - super(domElement, parentEditor.getRawOptions(), codeEditorWidgetOptions, contextKeyService, instantiationService, codeEditorService); + super(domElement, parentEditor.getRawOptions(), codeEditorWidgetOptions, contextKeyService, instantiationService, codeEditorService, audioCueService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 70dd0b515dbd5..d12ad4189c757 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -195,7 +195,7 @@ const editorConfiguration: IConfigurationNode = { 'diffEditor.diffAlgorithm': { type: 'string', enum: ['legacy', 'advanced'], - default: 'legacy', + default: 'advanced', markdownEnumDescriptions: [ nls.localize('diffAlgorithm.legacy', "Uses the legacy diffing algorithm."), nls.localize('diffAlgorithm.advanced', "Uses the advanced diffing algorithm."), @@ -216,6 +216,7 @@ const editorConfiguration: IConfigurationNode = { type: 'boolean', default: false, description: nls.localize('useVersion2', "Controls whether the diff editor uses the new or the old implementation."), + tags: ['experimental'], }, 'diffEditor.experimental.showEmptyDecorations': { type: 'boolean', diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 96dbb05c27523..5f30ccd744d22 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -827,6 +827,11 @@ export interface IDiffEditorBaseOptions { * Defaults to false */ isInEmbeddedEditor?: boolean; + + /** + * If the diff editor should only show the difference review mode. + */ + onlyShowAccessibleDiffViewer?: boolean; } /** diff --git a/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts b/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts index 0886c2da1e6a9..61a05010ea2d8 100644 --- a/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts +++ b/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts @@ -33,8 +33,11 @@ export function smoothenSequenceDiffs(sequence1: ISequence, sequence2: ISequence return result; } -export function randomRandomMatches(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { +export function removeRandomMatches(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { let diffs = sequenceDiffs; + if (diffs.length === 0) { + return diffs; + } let counter = 0; let shouldRepeat: boolean; diff --git a/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/src/vs/editor/common/diff/standardLinesDiffComputer.ts index 942308e0f0916..d73dda91bc13e 100644 --- a/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -11,7 +11,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { DateTimeout, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from 'vs/editor/common/diff/algorithms/diffAlgorithm'; import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/algorithms/dynamicProgrammingDiffing'; -import { optimizeSequenceDiffs, randomRandomMatches, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs'; +import { optimizeSequenceDiffs, removeRandomMatches, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/algorithms/myersDiffAlgorithm'; import { ILinesDiffComputer, ILinesDiffComputerOptions, LineRangeMapping, LinesDiff, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; @@ -169,6 +169,35 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } } + // Make sure all ranges are valid + assertFn(() => { + function validatePosition(pos: Position, lines: string[]): boolean { + if (pos.lineNumber < 1 || pos.lineNumber > lines.length) { return false; } + const line = lines[pos.lineNumber - 1]; + if (pos.column < 1 || pos.column > line.length + 1) { return false; } + return true; + } + + function validateRange(range: LineRange, lines: string[]): boolean { + if (range.startLineNumber < 1 || range.startLineNumber > lines.length + 1) { return false; } + if (range.endLineNumberExclusive < 1 || range.endLineNumberExclusive > lines.length + 1) { return false; } + return true; + } + + for (const c of changes) { + if (!c.innerChanges) { return false; } + for (const ic of c.innerChanges) { + const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) && + validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines); + if (!valid) { return false; } + } + if (!validateRange(c.modifiedRange, modifiedLines) || !validateRange(c.originalRange, originalLines)) { + return false; + } + } + return true; + }); + return new LinesDiff(changes, moves, hitTimeout); } @@ -184,7 +213,7 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { diffs = optimizeSequenceDiffs(slice1, slice2, diffs); diffs = coverFullWords(slice1, slice2, diffs); diffs = smoothenSequenceDiffs(slice1, slice2, diffs); - diffs = randomRandomMatches(slice1, slice2, diffs); + diffs = removeRandomMatches(slice1, slice2, diffs); const result = diffs.map( (d) => diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index 899ad35781f3e..56bd035e9d047 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -27,6 +27,7 @@ export namespace EditorContextKeys { export const readOnly = new RawContextKey('editorReadonly', false, nls.localize('editorReadonly', "Whether the editor is read-only")); export const inDiffEditor = new RawContextKey('inDiffEditor', false, nls.localize('inDiffEditor', "Whether the context is a diff editor")); export const isEmbeddedDiffEditor = new RawContextKey('isEmbeddedDiffEditor', false, nls.localize('isEmbeddedDiffEditor', "Whether the context is an embedded diff editor")); + export const accessibleDiffViewerVisible = new RawContextKey('accessibleDiffViewerVisible', false, nls.localize('accessibleDiffViewerVisible', "Whether the accessible diff viewer is visible")); 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 246d32bddd6d2..2e1b61be5d803 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -80,22 +80,6 @@ export class EncodedTokenizationResult { } } -/** - * @internal - */ -export interface IBackgroundTokenizer extends IDisposable { - /** - * Instructs the background tokenizer to set the tokens for the given range again. - * - * This might be necessary if the renderer overwrote those tokens with heuristically computed ones for some viewport, - * when the change does not even propagate to that viewport. - */ - requestTokens(startLineNumber: number, endLineNumberExclusive: number): void; - - reportMismatchingTokens?(lineNumber: number): void; -} - - /** * @internal */ @@ -118,6 +102,21 @@ export interface ITokenizationSupport { createBackgroundTokenizer?(textModel: model.ITextModel, store: IBackgroundTokenizationStore): IBackgroundTokenizer | undefined; } +/** + * @internal + */ +export interface IBackgroundTokenizer extends IDisposable { + /** + * Instructs the background tokenizer to set the tokens for the given range again. + * + * This might be necessary if the renderer overwrote those tokens with heuristically computed ones for some viewport, + * when the change does not even propagate to that viewport. + */ + requestTokens(startLineNumber: number, endLineNumberExclusive: number): void; + + reportMismatchingTokens?(lineNumber: number): void; +} + /** * @internal */ diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index deb5fbf628f37..0139d8b20558b 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IdleDeadline, runWhenIdle } from 'vs/base/common/async'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { setTimeout0 } from 'vs/base/common/platform'; import { StopWatch } from 'vs/base/common/stopwatch'; import { countEOL } from 'vs/editor/common/core/eolCounter'; @@ -25,7 +25,7 @@ const enum Constants { } export class TokenizerWithStateStore { - private readonly initialState = this.tokenizationSupport.getInitialState(); + private readonly initialState = this.tokenizationSupport.getInitialState() as TState; public readonly store: TrackingTokenizationStateStore; @@ -37,10 +37,11 @@ export class TokenizerWithStateStore { } public getStartState(lineNumber: number): TState | null { - if (lineNumber === 1) { - return this.initialState as TState; - } - return this.store.getEndState(lineNumber - 1); + return this.store.getStartState(lineNumber, this.initialState); + } + + public getFirstInvalidLine(): { lineNumber: number; startState: TState } | null { + return this.store.getFirstInvalidLine(this.initialState); } } @@ -58,17 +59,16 @@ export class TokenizerWithStateStoreAndTextModel const languageId = this._textModel.getLanguageId(); while (true) { - const nextLineNumber = this.store.getFirstInvalidEndStateLineNumber(); - if (!nextLineNumber || nextLineNumber > lineNumber) { + const lineToTokenize = this.getFirstInvalidLine(); + if (!lineToTokenize || lineToTokenize.lineNumber > lineNumber) { break; } - const text = this._textModel.getLineContent(nextLineNumber); - const lineStartState = this.getStartState(nextLineNumber); + const text = this._textModel.getLineContent(lineToTokenize.lineNumber); - const r = safeTokenize(this._languageIdCodec, languageId, this.tokenizationSupport, text, true, lineStartState!); - builder.add(nextLineNumber, r.tokens); - this!.store.setEndState(nextLineNumber, r.endState as TState); + const r = safeTokenize(this._languageIdCodec, languageId, this.tokenizationSupport, text, true, lineToTokenize.startState); + builder.add(lineToTokenize.lineNumber, r.tokens); + this.store.setEndState(lineToTokenize.lineNumber, r.endState as TState); } } @@ -204,8 +204,13 @@ export class TokenizerWithStateStoreAndTextModel } } +/** + * **Invariant:** + * If the text model is retokenized from line 1 to {@link getFirstInvalidEndStateLineNumber}() - 1, + * then the recomputed end state for line l will be equal to {@link getEndState}(l). + */ export class TrackingTokenizationStateStore { - private readonly tokenizationStateStore = new TokenizationStateStore(); + private readonly _tokenizationStateStore = new TokenizationStateStore(); private readonly _invalidEndStatesLineNumbers = new RangePriorityQueueImpl(); constructor(private lineCount: number) { @@ -213,20 +218,19 @@ export class TrackingTokenizationStateStore { } public getEndState(lineNumber: number): TState | null { - return this.tokenizationStateStore.getEndState(lineNumber); + return this._tokenizationStateStore.getEndState(lineNumber); } + /** + * @returns if the end state has changed. + */ public setEndState(lineNumber: number, state: TState): boolean { - while (true) { - const min = this._invalidEndStatesLineNumbers.min; - if (min !== null && min <= lineNumber) { - this._invalidEndStatesLineNumbers.removeMin(); - } else { - break; - } + if (!state) { + throw new BugIndicatingError('Cannot set null/undefined state'); } - const r = this.tokenizationStateStore.setEndState(lineNumber, state); + this._invalidEndStatesLineNumbers.delete(lineNumber); + const r = this._tokenizationStateStore.setEndState(lineNumber, state); if (r && lineNumber < this.lineCount) { // because the state changed, we cannot trust the next state anymore and have to invalidate it. this._invalidEndStatesLineNumbers.addRange(new OffsetRange(lineNumber + 1, lineNumber + 2)); @@ -237,7 +241,7 @@ export class TrackingTokenizationStateStore { public acceptChange(range: LineRange, newLineCount: number): void { this.lineCount += newLineCount - range.length; - this.tokenizationStateStore.acceptChange(range, newLineCount); + this._tokenizationStateStore.acceptChange(range, newLineCount); this._invalidEndStatesLineNumbers.addRangeAndResize(new OffsetRange(range.startLineNumber, range.endLineNumberExclusive), newLineCount); } @@ -252,16 +256,30 @@ export class TrackingTokenizationStateStore { this._invalidEndStatesLineNumbers.addRange(new OffsetRange(range.startLineNumber, range.endLineNumberExclusive)); } - public getFirstInvalidEndStateLineNumber(): number | null { - return this._invalidEndStatesLineNumbers.min; - } + public getFirstInvalidEndStateLineNumber(): number | null { return this._invalidEndStatesLineNumbers.min; } public getFirstInvalidEndStateLineNumberOrMax(): number { - return this._invalidEndStatesLineNumbers.min || Number.MAX_SAFE_INTEGER; + return this.getFirstInvalidEndStateLineNumber() || Number.MAX_SAFE_INTEGER; + } + + public allStatesValid(): boolean { return this._invalidEndStatesLineNumbers.min === null; } + + public getStartState(lineNumber: number, initialState: TState): TState | null { + if (lineNumber === 1) { return initialState; } + return this.getEndState(lineNumber - 1); } - public isTokenizationComplete(): boolean { - return this._invalidEndStatesLineNumbers.min === null; + public getFirstInvalidLine(initialState: TState): { lineNumber: number; startState: TState } | null { + const lineNumber = this.getFirstInvalidEndStateLineNumber(); + if (lineNumber === null) { + return null; + } + const startState = this.getStartState(lineNumber, initialState); + if (!startState) { + throw new BugIndicatingError('Start state must be defined'); + } + + return { lineNumber, startState }; } } @@ -338,6 +356,26 @@ export class RangePriorityQueueImpl implements RangePriorityQueue { return range.start; } + public delete(value: number): void { + const idx = this._ranges.findIndex(r => r.contains(value)); + if (idx !== -1) { + const range = this._ranges[idx]; + if (range.start === value) { + if (range.endExclusive === value + 1) { + this._ranges.splice(idx, 1); + } else { + this._ranges[idx] = new OffsetRange(value + 1, range.endExclusive); + } + } else { + if (range.endExclusive === value + 1) { + this._ranges[idx] = new OffsetRange(range.start, value); + } else { + this._ranges.splice(idx, 1, new OffsetRange(range.start, value), new OffsetRange(value + 1, range.endExclusive)); + } + } + } + } + public addRange(range: OffsetRange): void { OffsetRange.addRange(range, this._ranges); } @@ -490,23 +528,23 @@ export class DefaultBackgroundTokenizer implements IBackgroundTokenizer { if (!this._tokenizerWithStateStore) { return false; } - return !this._tokenizerWithStateStore.store.isTokenizationComplete(); + return !this._tokenizerWithStateStore.store.allStatesValid(); } private _tokenizeOneInvalidLine(builder: ContiguousMultilineTokensBuilder): number { - if (!this._tokenizerWithStateStore || !this._hasLinesToTokenize()) { + const firstInvalidLine = this._tokenizerWithStateStore?.getFirstInvalidLine(); + if (!firstInvalidLine) { return this._tokenizerWithStateStore._textModel.getLineCount() + 1; } - const lineNumber = this._tokenizerWithStateStore.store.getFirstInvalidEndStateLineNumber()!; - this._tokenizerWithStateStore.updateTokensUntilLine(builder, lineNumber); - return lineNumber; + this._tokenizerWithStateStore.updateTokensUntilLine(builder, firstInvalidLine.lineNumber); + return firstInvalidLine.lineNumber; } public checkFinished(): void { if (this._isDisposed) { return; } - if (this._tokenizerWithStateStore.store.isTokenizationComplete()) { + if (this._tokenizerWithStateStore.store.allStatesValid()) { this._backgroundTokenStore.backgroundTokenizationFinished(); } } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index c203f53ad7647..54476c26f2017 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -449,12 +449,10 @@ class GrammarTokens extends Disposable { this._onDidChangeBackgroundTokenizationState.fire(); }, setEndState: (lineNumber, state) => { - if (!state) { - throw new BugIndicatingError(); - } - const firstInvalidEndStateLineNumber = this._tokenizer?.store.getFirstInvalidEndStateLineNumber() ?? undefined; - if (firstInvalidEndStateLineNumber !== undefined && lineNumber >= firstInvalidEndStateLineNumber) { - // Don't accept states for definitely valid states + if (!this._tokenizer) { return; } + const firstInvalidEndStateLineNumber = this._tokenizer.store.getFirstInvalidEndStateLineNumber(); + // Don't accept states for definitely valid states, the renderer is ahead of the worker! + if (firstInvalidEndStateLineNumber !== null && lineNumber >= firstInvalidEndStateLineNumber) { this._tokenizer?.store.setEndState(lineNumber, state); } }, diff --git a/src/vs/editor/common/tokenizationRegistry.ts b/src/vs/editor/common/tokenizationRegistry.ts index 2d5ab7781a5a0..d9fb1bba82f2b 100644 --- a/src/vs/editor/common/tokenizationRegistry.ts +++ b/src/vs/editor/common/tokenizationRegistry.ts @@ -78,8 +78,6 @@ export class TokenizationRegistry implements ITokenizationRegistry { return this.get(languageId); } - - public isResolved(languageId: string): boolean { const tokenizationSupport = this.get(languageId); if (tokenizationSupport) { diff --git a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts index e321fae6dfd53..982ecbf505324 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts @@ -20,6 +20,7 @@ import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRend import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { Dimension } from 'vs/base/browser/dom'; export class ColorHover implements IHoverPart { @@ -181,6 +182,10 @@ function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPi if (hoverParts.length === 0 || !editor.hasModel()) { return Disposable.None; } + if (context.setMinimumDimensions) { + const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8; + context.setMinimumDimensions(new Dimension(302, minimumHeight)); + } const disposables = new DisposableStore(); const colorHover = hoverParts[0]; diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 4eef122381b1f..e6f6c2b49e4b2 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { IMouseEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; @@ -103,7 +103,7 @@ export class ContextMenuController implements IEditorContribution { e.event.stopPropagation(); if (e.target.type === MouseTargetType.SCROLLBAR) { - return this._showScrollbarContextMenu({ x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2 }); + return this._showScrollbarContextMenu(e.event); } if (e.target.type !== MouseTargetType.CONTENT_TEXT && e.target.type !== MouseTargetType.CONTENT_EMPTY && e.target.type !== MouseTargetType.TEXTAREA) { @@ -129,16 +129,16 @@ export class ContextMenuController implements IEditorContribution { } // Unless the user triggerd the context menu through Shift+F10, use the mouse position as menu position - let anchor: IAnchor | null = null; + let anchor: IMouseEvent | null = null; if (e.target.type !== MouseTargetType.TEXTAREA) { - anchor = { x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2 }; + anchor = e.event; } // Show the context menu this.showContextMenu(anchor); } - public showContextMenu(anchor?: IAnchor | null): void { + public showContextMenu(anchor?: IMouseEvent | null): void { if (!this._editor.getOption(EditorOption.contextmenu)) { return; // Context menu is turned off through configuration } @@ -193,7 +193,7 @@ export class ContextMenuController implements IEditorContribution { return result; } - private _doShowContextMenu(actions: IAction[], anchor: IAnchor | null = null): void { + private _doShowContextMenu(actions: IAction[], event: IMouseEvent | null = null): void { if (!this._editor.hasModel()) { return; } @@ -206,6 +206,7 @@ export class ContextMenuController implements IEditorContribution { } }); + let anchor: IMouseEvent | IAnchor | null = event; if (!anchor) { // Ensure selection is visible this._editor.revealPosition(this._editor.getPosition(), ScrollType.Immediate); @@ -259,7 +260,7 @@ export class ContextMenuController implements IEditorContribution { }); } - private _showScrollbarContextMenu(anchor: IAnchor): void { + private _showScrollbarContextMenu(anchor: IMouseEvent): void { if (!this._editor.hasModel()) { return; } diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts index 18e1b5f9d2376..d852003494c71 100644 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ b/src/vs/editor/contrib/hover/browser/contentHover.ts @@ -289,6 +289,7 @@ export class ContentHoverController extends Disposable { statusBar, setColorPicker: (widget) => colorPicker = widget, onContentsChanged: () => this._widget.onContentsChanged(), + setMinimumDimensions: (dimensions: dom.Dimension) => this._widget.setMinimumDimensions(dimensions), hide: () => this.hide() }; @@ -556,12 +557,18 @@ export class ContentHoverWidget extends ResizableContentWidget { this._layoutContentWidget(); } - private _setContentsDomNodeMaxDimensions(width: number | string, height: number | string): void { + private static _applyMaxDimensions(container: HTMLElement, width: number | string, height: number | string) { const transformedWidth = typeof width === 'number' ? `${width}px` : width; const transformedHeight = typeof height === 'number' ? `${height}px` : height; - const contentsDomNode = this._hover.contentsDomNode; - contentsDomNode.style.maxWidth = transformedWidth; - contentsDomNode.style.maxHeight = transformedHeight; + container.style.maxWidth = transformedWidth; + container.style.maxHeight = transformedHeight; + } + + private _setHoverWidgetMaxDimensions(width: number | string, height: number | string): void { + ContentHoverWidget._applyMaxDimensions(this._hover.contentsDomNode, width, height); + ContentHoverWidget._applyMaxDimensions(this._hover.containerDomNode, width, height); + this._hover.containerDomNode.style.setProperty('--hover-maxWidth', typeof width === 'number' ? `${width}px` : width); + this._layoutContentWidget(); } private _hasHorizontalScrollbar(): boolean { @@ -579,7 +586,7 @@ export class ContentHoverWidget extends ResizableContentWidget { } private _setAdjustedHoverWidgetDimensions(size: dom.Dimension): void { - this._setContentsDomNodeMaxDimensions('none', 'none'); + this._setHoverWidgetMaxDimensions('none', 'none'); const width = size.width; const height = size.height; this._setHoverWidgetDimensions(width, height); @@ -594,6 +601,7 @@ export class ContentHoverWidget extends ResizableContentWidget { const maxRenderingWidth = this._findMaximumRenderingWidth() ?? Infinity; const maxRenderingHeight = this._findMaximumRenderingHeight() ?? Infinity; this._resizableNode.maxSize = new dom.Dimension(maxRenderingWidth, maxRenderingHeight); + this._setHoverWidgetMaxDimensions(maxRenderingWidth, maxRenderingHeight); } protected override _resize(size: dom.Dimension): void { @@ -670,13 +678,11 @@ export class ContentHoverWidget extends ResizableContentWidget { } private _layout(): void { - const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height); - const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 500, ContentHoverWidget._lastDimensions.width); const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); const contentsDomNode = this._hover.contentsDomNode; contentsDomNode.style.fontSize = `${fontSize}px`; contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`; - this._setContentsDomNodeMaxDimensions(width, height); + this._updateMaxDimensions(); } private _updateFont(): void { @@ -696,17 +702,17 @@ export class ContentHoverWidget extends ResizableContentWidget { this._hover.onContentsChanged(); } - private _updateContentsDomNodeMaxDimensions() { + private _updateMaxDimensions() { const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height); const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 500, ContentHoverWidget._lastDimensions.width); - this._setContentsDomNodeMaxDimensions(width, height); + this._setHoverWidgetMaxDimensions(width, height); } private _render(node: DocumentFragment, hoverData: ContentHoverVisibleData) { this._setHoverData(hoverData); this._updateFont(); this._updateContent(node); - this._updateContentsDomNodeMaxDimensions(); + this._updateMaxDimensions(); this.onContentsChanged(); // Simply force a synchronous render on the editor // such that the widget does not really render with left = '0px' @@ -773,6 +779,10 @@ export class ContentHoverWidget extends ResizableContentWidget { this._setContentsDomNodeDimensions(dom.getTotalWidth(contentsDomNode), Math.min(maxRenderingHeight, height - SCROLLBAR_WIDTH)); } + public setMinimumDimensions(dimensions: dom.Dimension): void { + this._resizableNode.minSize = dimensions; + } + public onContentsChanged(): void { this._removeConstraintsRenderNormally(); const containerDomNode = this._hover.containerDomNode; @@ -791,6 +801,10 @@ export class ContentHoverWidget extends ResizableContentWidget { this._adjustContentsBottomPadding(); this._adjustHoverHeightForScrollbar(height); } + if (this._visibleData?.showAtPosition) { + const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); + this._positionPreference = this._findPositionPreference(widgetHeight, this._visibleData.showAtPosition); + } this._layoutContentWidget(); } diff --git a/src/vs/editor/contrib/hover/browser/hover.ts b/src/vs/editor/contrib/hover/browser/hover.ts index 2c1e2b2611e7e..43969c828b5af 100644 --- a/src/vs/editor/contrib/hover/browser/hover.ts +++ b/src/vs/editor/contrib/hover/browser/hover.ts @@ -17,7 +17,7 @@ import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/go import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; import { ContentHoverWidget, ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHover'; import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHover'; -import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; +import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -31,6 +31,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import * as nls from 'vs/nls'; import 'vs/css!./hover'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { status } from 'vs/base/browser/ui/aria/aria'; // sticky hover widget which doesn't disappear on focus out and such const _sticky = false @@ -361,6 +363,9 @@ class ShowOrFocusHoverAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + const configurationService = accessor.get(IConfigurationService); + const accessibilityService = accessor.get(IAccessibilityService); + const keybindingService = accessor.get(IKeybindingService); if (!editor.hasModel()) { return; } @@ -377,6 +382,11 @@ class ShowOrFocusHoverAction extends EditorAction { } else { controller.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Keyboard, focus); } + if (configurationService.getValue('accessibility.verbosity.hover') && accessibilityService.isScreenReaderOptimized()) { + const keybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); + const hint = keybinding ? nls.localize('chatAccessibleViewHint', "Inspect this in the accessible view with {0}", keybinding) : nls.localize('chatAccessibleViewHintNoKb', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding"); + status(hint); + } } } diff --git a/src/vs/editor/contrib/hover/browser/hoverTypes.ts b/src/vs/editor/contrib/hover/browser/hoverTypes.ts index 93d4104b197dd..aadc1851a011f 100644 --- a/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Dimension } from 'vs/base/browser/dom'; import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -110,6 +111,10 @@ export interface IEditorHoverRenderContext { * The contents rendered inside the fragment have been changed, which means that the hover should relayout. */ onContentsChanged(): void; + /** + * Set the minimum dimensions of the resizable hover + */ + setMinimumDimensions?(dimensions: Dimension): void; /** * Hide the hover. */ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys.ts index b027df1b39cd3..30556a12cc6c6 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys.ts @@ -30,12 +30,13 @@ export class InlineCompletionContextKeys extends Disposable { this._register(autorun('update context key: inlineCompletionVisible, suppressSuggestions', (reader) => { const model = this.model.read(reader); - const suggestion = model?.selectedInlineCompletion.read(reader); - const ghostText = model?.ghostText.read(reader); - this.inlineCompletionVisible.set(ghostText !== undefined && !ghostText.isEmpty()); + const state = model?.state.read(reader); + + const isInlineCompletionVisible = !!state?.inlineCompletion && state?.ghostText !== undefined && !state?.ghostText.isEmpty(); + this.inlineCompletionVisible.set(isInlineCompletionVisible); - if (ghostText && suggestion) { - this.suppressSuggestions.set(suggestion.inlineCompletion.source.inlineCompletions.suppressSuggestions); + if (state?.ghostText && state?.inlineCompletion) { + this.suppressSuggestions.set(state.inlineCompletion.inlineCompletion.source.inlineCompletions.suppressSuggestions); } })); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts index e8699277b347f..d764b7b0d751b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts @@ -172,16 +172,16 @@ export class InlineCompletionsController extends Disposable { this._register(autorun('play audio cue & read suggestion', reader => { const model = this.model.read(reader); const state = model?.state.read(reader); - if (!model || !state || !state.completion) { + if (!model || !state || !state.inlineCompletion) { lastInlineCompletionId = undefined; return; } - if (state.completion.semanticId !== lastInlineCompletionId) { - lastInlineCompletionId = state.completion.semanticId; + if (state.inlineCompletion.semanticId !== lastInlineCompletionId) { + lastInlineCompletionId = state.inlineCompletion.semanticId; + const lineText = model.textModel.getLineContent(state.ghostText.lineNumber); this.audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => { if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - const lineText = model.textModel.getLineContent(state.ghostText.lineNumber); alert(state.ghostText.renderForScreenReader(lineText)); } }); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 05d6de2fed1f9..58e38cd7a51d1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -65,7 +65,7 @@ export class InlineCompletionsModel extends Disposable { let lastItem: InlineCompletionWithUpdatedRange | undefined = undefined; this._register(autorun('call handleItemDidShow', reader => { const item = this.state.read(reader); - const completion = item?.completion; + const completion = item?.inlineCompletion; if (completion?.semanticId !== lastItem?.semanticId) { lastItem = completion; if (completion) { @@ -191,7 +191,7 @@ export class InlineCompletionsModel extends Disposable { public readonly state = derived<{ suggestItem: SuggestItemInfo | undefined; - completion: InlineCompletionWithUpdatedRange | undefined; + inlineCompletion: InlineCompletionWithUpdatedRange | undefined; ghostText: GhostTextOrReplacement; } | undefined>('ghostTextAndCompletion', (reader) => { const model = this.textModel; @@ -225,7 +225,7 @@ export class InlineCompletionsModel extends Disposable { // Show an invisible ghost text to reserve space const ghostText = newGhostText ?? new GhostText(edit.range.endLineNumber, []); - return { ghostText, completion: augmentedCompletion?.completion, suggestItem }; + return { ghostText, inlineCompletion: augmentedCompletion?.completion, suggestItem }; } else { if (!this._isActive.read(reader)) { return undefined; } const item = this.selectedInlineCompletion.read(reader); @@ -235,7 +235,7 @@ export class InlineCompletionsModel extends Disposable { const mode = this._inlineSuggestMode.read(reader); const cursor = this.cursorPosition.read(reader); const ghostText = replacement.computeGhostText(model, mode, cursor); - return ghostText ? { ghostText, completion: item, suggestItem: undefined } : undefined; + return ghostText ? { ghostText, inlineCompletion: item, suggestItem: undefined } : undefined; } }); @@ -271,10 +271,10 @@ export class InlineCompletionsModel extends Disposable { } const state = this.state.get(); - if (!state || state.ghostText.isEmpty() || !state.completion) { + if (!state || state.ghostText.isEmpty() || !state.inlineCompletion) { return; } - const completion = state.completion.toInlineCompletion(undefined); + const completion = state.inlineCompletion.toInlineCompletion(undefined); editor.pushUndoStop(); if (completion.snippetInfo) { @@ -363,11 +363,11 @@ export class InlineCompletionsModel extends Disposable { } const state = this.state.get(); - if (!state || state.ghostText.isEmpty() || !state.completion) { + if (!state || state.ghostText.isEmpty() || !state.inlineCompletion) { return; } const ghostText = state.ghostText; - const completion = state.completion.toInlineCompletion(undefined); + const completion = state.inlineCompletion.toInlineCompletion(undefined); if (completion.snippetInfo || completion.filterText !== completion.insertText) { // not in WYSIWYG mode, partial commit might change completion, thus it is not supported diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts index c844e63e8a17b..af311ba716a5c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts @@ -221,7 +221,13 @@ export class UpToDateInlineCompletions implements IDisposable { public dispose(): void { this._refCount--; if (this._refCount === 0) { - this.textModel.deltaDecorations(this._inlineCompletions.map(i => i.decorationId), []); + setTimeout(() => { + // To fix https://github.com/microsoft/vscode/issues/188348 + if (!this.textModel.isDisposed()) { + // This is just cleanup. It's ok if it happens with a delay. + this.textModel.deltaDecorations(this._inlineCompletions.map(i => i.decorationId), []); + } + }, 0); this.inlineCompletionProviderResult.dispose(); for (const i of this._prependedInlineCompletionItems) { i.source.removeRef(); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index fb7adfb064a0f..132fb4ff46cee 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -26,6 +26,7 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import * as dom from 'vs/base/browser/dom'; import { StickyRange } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollElement'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; interface CustomMouseEvent { detail: string; @@ -290,7 +291,9 @@ export class StickyScrollController extends Disposable implements IEditorContrib return linkGestureStore; } - private _onContextMenu(event: MouseEvent) { + private _onContextMenu(e: MouseEvent) { + const event = new StandardMouseEvent(e); + this._contextMenuService.showContextMenu({ menuId: MenuId.StickyScrollContext, getAnchor: () => event, diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 43e1f1eef10e8..d9a8d83cf09fa 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -38,6 +38,7 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; +import { IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; /** * Description of an action contribution @@ -572,7 +573,8 @@ export class StandaloneDiffEditor2 extends DiffEditorWidget2 implements IStandal @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @IEditorProgressService editorProgressService: IEditorProgressService, - @IClipboardService clipboardService: IClipboardService + @IClipboardService clipboardService: IClipboardService, + @IAudioCueService audioCueService: IAudioCueService, ) { const options = { ..._options }; updateConfigurationService(configurationService, options, true); @@ -591,6 +593,7 @@ export class StandaloneDiffEditor2 extends DiffEditorWidget2 implements IStandal contextKeyService, instantiationService, codeEditorService, + audioCueService, ); this._configurationService = configurationService; diff --git a/src/vs/editor/standalone/browser/standaloneThemeService.ts b/src/vs/editor/standalone/browser/standaloneThemeService.ts index ec49af70ecebf..a391249e6e0cb 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeService.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeService.ts @@ -392,7 +392,7 @@ export class StandaloneThemeService extends Disposable implements IStandaloneThe colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`); } } - ruleCollector.addRule(`.monaco-editor { ${colorVariables.join('\n')} }`); + ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor { ${colorVariables.join('\n')} }`); const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap(); ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index dbcada860e609..8d88aa92a7fdd 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3975,6 +3975,10 @@ declare namespace monaco.editor { * Defaults to false */ isInEmbeddedEditor?: boolean; + /** + * If the diff editor should only show the difference review mode. + */ + onlyShowAccessibleDiffViewer?: boolean; } /** @@ -6129,8 +6133,8 @@ declare namespace monaco.editor { * Update the editor's options after the editor has been created. */ updateOptions(newOptions: IDiffEditorOptions): void; - diffReviewNext(): void; - diffReviewPrev(): void; + accessibleDiffViewerNext(): void; + accessibleDiffViewerPrev(): void; } export class FontInfo extends BareFontInfo { diff --git a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index ea41cc6e226a6..ea66a1e2c502e 100644 --- a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts +++ b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -18,6 +18,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export interface IDropdownWithPrimaryActionViewItemOptions { getKeyBinding?: (action: IAction) => ResolvedKeybinding | undefined; @@ -43,10 +44,11 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { @IKeybindingService _keybindingService: IKeybindingService, @INotificationService _notificationService: INotificationService, @IContextKeyService _contextKeyService: IContextKeyService, - @IThemeService _themeService: IThemeService + @IThemeService _themeService: IThemeService, + @IAccessibilityService _accessibilityService: IAccessibilityService ) { super(null, primaryAction); - this._primaryAction = new MenuEntryActionViewItem(primaryAction, undefined, _keybindingService, _notificationService, _contextKeyService, _themeService, _contextMenuProvider); + this._primaryAction = new MenuEntryActionViewItem(primaryAction, undefined, _keybindingService, _notificationService, _contextKeyService, _themeService, _contextMenuProvider, _accessibilityService); this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { menuAsChild: true, classNames: className ? ['codicon', 'codicon-chevron-down', className] : ['codicon', 'codicon-chevron-down'], diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 54400220a8e02..356ff5e101311 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -30,6 +30,7 @@ import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { assertType } from 'vs/base/common/types'; import { asCssVariable, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void { const groups = menu.getActions(options); @@ -126,7 +127,8 @@ export class MenuEntryActionViewItem extends ActionViewItem { @INotificationService protected _notificationService: INotificationService, @IContextKeyService protected _contextKeyService: IContextKeyService, @IThemeService protected _themeService: IThemeService, - @IContextMenuService protected _contextMenuService: IContextMenuService + @IContextMenuService protected _contextMenuService: IContextMenuService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding, hoverDelegate: options?.hoverDelegate }); this._altKey = ModifierKeyEmitter.getInstance(); @@ -160,13 +162,14 @@ export class MenuEntryActionViewItem extends ActionViewItem { } if (this._menuItemAction.alt) { - let mouseOverOnWindowsOrLinux = false; + let isMouseOver = false; const updateAltState = () => { - const wantsAltCommand = !!this._menuItemAction.alt?.enabled && ( - this._altKey.keyStatus.altKey - || (this._altKey.keyStatus.shiftKey && mouseOverOnWindowsOrLinux) - ); + const wantsAltCommand = !!this._menuItemAction.alt?.enabled && + (!this._accessibilityService.isMotionReduced() || isMouseOver) && ( + this._altKey.keyStatus.altKey || + (this._altKey.keyStatus.shiftKey && isMouseOver) + ); if (wantsAltCommand !== this._wantsAltCommand) { this._wantsAltCommand = wantsAltCommand; @@ -178,17 +181,15 @@ export class MenuEntryActionViewItem extends ActionViewItem { this._register(this._altKey.event(updateAltState)); - if (isWindows || isLinux) { - this._register(addDisposableListener(container, 'mouseleave', _ => { - mouseOverOnWindowsOrLinux = false; - updateAltState(); - })); + this._register(addDisposableListener(container, 'mouseleave', _ => { + isMouseOver = false; + updateAltState(); + })); - this._register(addDisposableListener(container, 'mouseenter', _ => { - mouseOverOnWindowsOrLinux = true; - updateAltState(); - })); - } + this._register(addDisposableListener(container, 'mouseenter', _ => { + isMouseOver = true; + updateAltState(); + })); updateAltState(); } diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index 6df086344a7f3..e0a3a879b2052 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { addDisposableListener } from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IToolBarOptions, ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction, Separator, SubmenuAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { coalesceInPlace } from 'vs/base/common/arrays'; @@ -170,13 +171,14 @@ export class WorkbenchToolBar extends ToolBar { // add context menu for toggle actions if (toggleActions.length > 0) { this._sessionDisposables.add(addDisposableListener(this.getElement(), 'contextmenu', e => { + const event = new StandardMouseEvent(e); - const action = this.getItemAction(e.target); + const action = this.getItemAction(event.target); if (!(action)) { return; } - e.preventDefault(); - e.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); let noHide = false; @@ -232,7 +234,7 @@ export class WorkbenchToolBar extends ToolBar { } this._contextMenuService.showContextMenu({ - getAnchor: () => e, + getAnchor: () => event, getActions: () => actions, // add context menu actions (iff appicable) menuId: this._options?.contextMenu, diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index a6b2f612b3c04..9aa9e402f12c9 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -145,6 +145,7 @@ export class MenuId { static readonly InteractiveCellExecute = new MenuId('InteractiveCellExecute'); static readonly InteractiveInputExecute = new MenuId('InteractiveInputExecute'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); + static readonly NotebookStickyScrollContext = new MenuId('NotebookStickyScrollContext'); static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly NotebookCellDelete = new MenuId('NotebookCellDelete'); static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index 505d76eabfbdd..10158c8d75cc4 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; -import { AnchorAlignment, AnchorAxisAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { AnchorAlignment, AnchorAxisAlignment, IAnchor, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { IAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -29,7 +30,13 @@ export interface IContextViewDelegate { canRelayout?: boolean; // Default: true - getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number }; + /** + * The anchor where to position the context view. + * Use a `HTMLElement` to position the view at the element, + * a `StandardMouseEvent` to position it at the mouse position + * or an `IAnchor` to position it at a specific location. + */ + getAnchor(): HTMLElement | StandardMouseEvent | IAnchor; render(container: HTMLElement): IDisposable; onDOMEvent?(e: any, activeElement: HTMLElement): void; onHide?(data?: any): void; diff --git a/src/vs/platform/encryption/common/encryptionService.ts b/src/vs/platform/encryption/common/encryptionService.ts index 59d6b38501475..c00a32a663e77 100644 --- a/src/vs/platform/encryption/common/encryptionService.ts +++ b/src/vs/platform/encryption/common/encryptionService.ts @@ -25,6 +25,19 @@ export interface ICommonEncryptionService { isEncryptionAvailable(): Promise; } +// The values provided to the `password-store` command line switch. +// Notice that they are not the same as the values returned by +// `getSelectedStorageBackend` in the `safeStorage` API. +export const enum PasswordStoreCLIOption { + kwallet = 'kwallet', + kwallet5 = 'kwallet5', + gnome = 'gnome', + gnomeKeyring = 'gnome-keyring', + gnomeLibsecret = 'gnome-libsecret', + basic = 'basic' +} + +// The values returned by `getSelectedStorageBackend` in the `safeStorage` API. export const enum KnownStorageProvider { unknown = 'unknown', basicText = 'basic_text', @@ -37,6 +50,9 @@ export const enum KnownStorageProvider { kwallet5 = 'kwallet5', kwallet6 = 'kwallet6', + // The rest of these are not returned by `getSelectedStorageBackend` + // but these were added for platform completeness. + // Windows dplib = 'dpapi', diff --git a/src/vs/platform/encryption/electron-main/encryptionMainService.ts b/src/vs/platform/encryption/electron-main/encryptionMainService.ts index 0589dc473b943..215ef85d41f83 100644 --- a/src/vs/platform/encryption/electron-main/encryptionMainService.ts +++ b/src/vs/platform/encryption/electron-main/encryptionMainService.ts @@ -5,7 +5,7 @@ import { safeStorage as safeStorageElectron, app } from 'electron'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; -import { KnownStorageProvider, IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; +import { KnownStorageProvider, IEncryptionMainService, PasswordStoreCLIOption } from 'vs/platform/encryption/common/encryptionService'; import { ILogService } from 'vs/platform/log/common/log'; // These APIs are currently only supported in our custom build of electron so @@ -25,7 +25,7 @@ export class EncryptionMainService implements IEncryptionMainService { @ILogService private readonly logService: ILogService ) { // if this commandLine switch is set, the user has opted in to using basic text encryption - if (app.commandLine.getSwitchValue('password-store') === 'basic_text') { + if (app.commandLine.getSwitchValue('password-store') === PasswordStoreCLIOption.basic) { safeStorage.setUsePlainTextEncryption?.(true); } } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 2bbfd487d7546..17476d941872f 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -88,6 +88,7 @@ export interface NativeParsedArgs { 'install-source'?: string; 'disable-updates'?: boolean; 'disable-keytar'?: boolean; + 'password-store'?: string; 'disable-workspace-trust'?: boolean; 'disable-crash-reporter'?: boolean; 'crash-reporter-directory'?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index fbae6c3e4ed81..b3e423e20f9cf 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -139,6 +139,7 @@ export const OPTIONS: OptionDescriptions> = { 'disable-telemetry': { type: 'boolean' }, 'disable-updates': { type: 'boolean' }, 'disable-keytar': { type: 'boolean' }, + 'password-store': { type: 'string' }, 'disable-workspace-trust': { type: 'boolean' }, 'disable-crash-reporter': { type: 'boolean' }, 'crash-reporter-directory': { type: 'string' }, diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index bd10be78b29ff..9ed588ca6f1d8 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -14,6 +14,11 @@ function testErrorMessage(module: string): string { flakySuite('Native Modules (all platforms)', () => { + test('kerberos', async () => { + const kerberos = await import('kerberos'); + assert.ok(typeof kerberos.initializeClient === 'function', testErrorMessage('kerberos')); + }); + test('native-is-elevated', async () => { const isElevated = await import('native-is-elevated'); assert.ok(typeof isElevated === 'function', testErrorMessage('native-is-elevated ')); diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 96195aae781e1..16744885ac788 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -494,33 +494,46 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { + let compatibleExtension: IGalleryExtension | null; + const extensionsControlManifest = await this.getExtensionsControlManifest(); if (extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier))) { throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious); } - if (!await this.canInstall(extension)) { - const targetPlatform = await this.getTargetPlatform(); - 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); + 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]; + 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); + } } - const compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease); - 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]) { - throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); + else { + if (!await this.canInstall(extension)) { + const targetPlatform = await this.getTargetPlatform(); + 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); + 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]) { + throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); + } + throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible); } - throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible); } this.logService.info('Getting Manifest...', compatibleExtension.identifier.id); const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None); if (manifest === null) { - throw new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid); + throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, ExtensionManagementErrorCode.Invalid); } if (manifest.version !== compatibleExtension.version) { - throw new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid); + throw new ExtensionManagementError(`Cannot install '${compatibleExtension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid); } return { extension: compatibleExtension, manifest }; diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 30e749ac312f6..d3c27750a2cea 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -127,8 +127,8 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable await this.withProfileExtensions(profileLocation, profileExtensions => { const result: IScannedProfileExtension[] = []; for (const extension of profileExtensions) { - if (extensions.some(([e]) => areSameExtensions(e.identifier, extension.identifier) && e.manifest.version !== extension.version)) { - // Remove the existing extension with different version + if (extensions.some(([e]) => areSameExtensions(e.identifier, extension.identifier))) { + // Remove the existing extension extensionsToRemove.push(extension); } else { result.push(extension); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 5852d66a9c42e..78cbb3b772479 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -570,8 +570,10 @@ export class ExtensionsScanner extends Disposable { await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); } - removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { - return this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type); + async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { + if (this.uriIdentityService.extUri.isEqualOrParent(this.extensionsScannerService.userExtensionsLocation, extension.location)) { + return this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type); + } } async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { diff --git a/src/vs/platform/profiling/common/profilingTelemetrySpec.ts b/src/vs/platform/profiling/common/profilingTelemetrySpec.ts index 2fa784bbc0406..cd85a0d7385bc 100644 --- a/src/vs/platform/profiling/common/profilingTelemetrySpec.ts +++ b/src/vs/platform/profiling/common/profilingTelemetrySpec.ts @@ -67,11 +67,11 @@ class PerformanceError extends Error { readonly selfTime: number; constructor(data: SampleData) { - super('[PerfSampleError]'); + super(`PerfSampleError: by ${data.source} in ${data.sample.location}`); this.name = 'PerfSampleError'; this.selfTime = data.sample.selfTime; const trace = [data.sample.absLocation, ...data.sample.caller.map(c => c.absLocation)]; - this.stack = `${this.message} by ${data.source} in ${data.sample.location}\n\t at ${trace.join('\n\t at ')}`; + this.stack = `\n\t at ${trace.join('\n\t at ')}`; } } diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index c8158ad9b50d7..425a189da6c4b 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -1239,7 +1239,9 @@ class InputBox extends QuickInput implements IInputBox { const visibilities: Visibilities = { title: !!this.title || !!this.step || !!this.buttons.length, description: !!this.description || !!this.step, - inputBox: true, message: true + inputBox: true, + message: true, + progressBar: true }; this.ui.setVisibilities(visibilities); diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 42168d7a59c2d..125b79f2556b0 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -148,6 +148,11 @@ function registerProxyConfigurations(scope: ConfigurationScope): void { description: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs."), restricted: true }, + 'http.proxyKerberosServicePrincipal': { + type: 'string', + markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."), + restricted: true + }, 'http.proxyAuthorization': { type: ['null', 'string'], default: null, diff --git a/src/vs/platform/secrets/common/secrets.ts b/src/vs/platform/secrets/common/secrets.ts index 6c6b822ebc25d..4f9b9a49f9fce 100644 --- a/src/vs/platform/secrets/common/secrets.ts +++ b/src/vs/platform/secrets/common/secrets.ts @@ -128,7 +128,14 @@ export abstract class BaseSecretStorageService implements ISecretStorageService } this._onDidChangeValueDisposable?.dispose(); - this._onDidChangeValueDisposable = storageService.onDidChangeValue(e => this.onDidChangeValue(e.key)); + this._onDidChangeValueDisposable = storageService.onDidChangeValue(e => { + // We only care about changes to the application scope since SecretStorage + // only stores secrets in the application scope but this seems to fire + // 2 events. Once for APP scope and once for PROFILE scope. ref #188460 + if (e.scope === StorageScope.APPLICATION) { + this.onDidChangeValue(e.key); + } + }); return storageService; } diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 8f46ec11b6093..7ce224a4b9858 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -64,6 +64,10 @@ export interface ITunnelProvider { forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; } +export function isTunnelProvider(addressOrTunnelProvider: IAddressProvider | ITunnelProvider): addressOrTunnelProvider is ITunnelProvider { + return !!(addressOrTunnelProvider as ITunnelProvider).forwardPort; +} + export enum ProvidedOnAutoForward { Notify = 1, OpenBrowser = 2, @@ -315,7 +319,8 @@ export abstract class AbstractTunnelService implements ITunnelService { openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost?: string, localPort?: number, elevateIfNeeded: boolean = false, privacy?: string, protocol?: string): Promise | undefined { this.logService.trace(`ForwardedPorts: (TunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); - if (!addressProvider) { + const addressOrTunnelProvider = this._tunnelProvider ?? addressProvider; + if (!addressOrTunnelProvider) { return undefined; } @@ -332,7 +337,7 @@ export abstract class AbstractTunnelService implements ITunnelService { return; } - const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localHost, localPort, elevateIfNeeded, privacy, protocol); + const resolvedTunnel = this.retainOrCreateTunnel(addressOrTunnelProvider, remoteHost, remotePort, localHost, localPort, elevateIfNeeded, privacy, protocol); if (!resolvedTunnel) { this.logService.trace(`ForwardedPorts: (TunnelService) Tunnel was not created.`); return resolvedTunnel; @@ -454,7 +459,7 @@ export abstract class AbstractTunnelService implements ITunnelService { public abstract isPortPrivileged(port: number): boolean; - protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined; + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider | ITunnelProvider, remoteHost: string, remotePort: number, localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined; protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`); diff --git a/src/vs/platform/tunnel/node/tunnelService.ts b/src/vs/platform/tunnel/node/tunnelService.ts index 31f153b651a9d..8493fbc2fdfd8 100644 --- a/src/vs/platform/tunnel/node/tunnelService.ts +++ b/src/vs/platform/tunnel/node/tunnelService.ts @@ -14,7 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { connectRemoteAgentTunnel, IAddressProvider, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; -import { AbstractTunnelService, isAllInterfaces, ISharedTunnelsService as ISharedTunnelsService, isLocalhost, isPortPrivileged, ITunnelService, RemoteTunnel, TunnelPrivacyId } from 'vs/platform/tunnel/common/tunnel'; +import { AbstractTunnelService, isAllInterfaces, ISharedTunnelsService as ISharedTunnelsService, isLocalhost, isPortPrivileged, isTunnelProvider, ITunnelProvider, ITunnelService, RemoteTunnel, TunnelPrivacyId } from 'vs/platform/tunnel/common/tunnel'; import { ISignService } from 'vs/platform/sign/common/sign'; import { OS } from 'vs/base/common/platform'; import { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; @@ -168,21 +168,21 @@ export class BaseTunnelService extends AbstractTunnelService { return isPortPrivileged(port, this.defaultTunnelHost, OS, os.release()); } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { + protected retainOrCreateTunnel(addressOrTunnelProvider: IAddressProvider | ITunnelProvider, remoteHost: string, remotePort: number, localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; } - if (this._tunnelProvider) { - return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); + if (isTunnelProvider(addressOrTunnelProvider)) { + return this.createWithProvider(addressOrTunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); } else { this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`); const options: IConnectionOptions = { commit: this.productService.commit, quality: this.productService.quality, - addressProvider, + addressProvider: addressOrTunnelProvider, remoteSocketFactoryService: this.remoteSocketFactoryService, signService: this.signService, logService: this.logService, diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index 49c0d23827171..9ec07dceb5098 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -278,7 +278,7 @@ function areSame(fromExtension: ISyncExtension, toExtension: ISyncExtension, che return false; } - if (fromExtension.isApplicationScoped !== toExtension.isApplicationScoped) { + if (!!fromExtension.isApplicationScoped !== !!toExtension.isApplicationScoped) { /* extension application scope has changed */ return false; } @@ -401,23 +401,26 @@ function massageIncomingExtension(extension: ISyncExtension): ISyncExtension { // massage outgoing extension - remove optional properties function massageOutgoingExtension(extension: ISyncExtension, key: string): ISyncExtension { const massagedExtension: ISyncExtension = { + ...extension, identifier: { id: extension.identifier.id, uuid: key.startsWith('uuid:') ? key.substring('uuid:'.length) : undefined }, - version: extension.version, /* set following always so that to differentiate with older clients */ preRelease: !!extension.preRelease, - pinned: !!extension.pinned + pinned: !!extension.pinned, }; - if (extension.disabled) { - massagedExtension.disabled = true; + if (!extension.disabled) { + delete massagedExtension.disabled; } - if (extension.installed) { - massagedExtension.installed = true; + if (!extension.installed) { + delete massagedExtension.installed; } - if (extension.state) { - massagedExtension.state = extension.state; + if (!extension.state) { + delete massagedExtension.state; + } + if (!extension.isApplicationScoped) { + delete massagedExtension.isApplicationScoped; } return massagedExtension; } diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index aa22d90107dee..bd85da7b12c5e 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -101,7 +101,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse */ /* Version 4: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ /* Version 5: Introduce extension state */ - protected readonly version: number = 5; + /* Version 6: Added isApplicationScoped property */ + protected readonly version: number = 6; private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'extensions.json'); private readonly baseResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' }); @@ -377,7 +378,7 @@ export class LocalExtensionsProvider { .map(extension => { const { identifier, isBuiltin, manifest, preRelease, pinned, isApplicationScoped } = extension; const syncExntesion: ILocalSyncExtension = { identifier, preRelease, version: manifest.version, pinned: !!pinned }; - if (!isApplicationScopedExtension(manifest)) { + if (isApplicationScoped && !isApplicationScopedExtension(manifest)) { syncExntesion.isApplicationScoped = isApplicationScoped; } if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) { diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index 606983a1c0acf..d655cf5984e2b 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -52,6 +52,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i @IUriIdentityService uriIdentityService: IUriIdentityService, ) { super({ syncResource: SyncResource.Profiles, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService); + this._register(userDataProfilesService.onDidChangeProfiles(() => this.triggerLocalChange())); } async getLastSyncedProfiles(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 32cbc4dba51b1..a58320c263efd 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -423,9 +423,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } const updatedRemoteProfiles = remoteProfiles.filter(profile => allCollections.includes(profile.collection)); if (updatedRemoteProfiles.length !== remoteProfiles.length) { - this.logService.info(`Updating remote profiles with invalid collections on server`); const profileManifestSynchronizer = this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.userDataProfilesService.defaultProfile, undefined); try { + this.logService.info('Resetting the last synced state of profiles'); + await profileManifestSynchronizer.resetLocal(); + this.logService.info('Did reset the last synced state of profiles'); + this.logService.info(`Updating remote profiles with invalid collections on server`); await profileManifestSynchronizer.updateRemoteProfiles(updatedRemoteProfiles, null); this.logService.info(`Updated remote profiles on server`); } finally { diff --git a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts index 94bb9f900527a..34d05f3a3a6ce 100644 --- a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts @@ -69,7 +69,7 @@ suite('ExtensionsMerge', () => { aLocalSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), ]; const skippedExtension = [ - anSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), + aSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), ]; const expected = [...localExtensions]; @@ -88,7 +88,7 @@ suite('ExtensionsMerge', () => { aLocalSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), ]; const skippedExtension = [ - anSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), + aSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), ]; const expected = [localExtensions[1], localExtensions[2]]; @@ -106,8 +106,8 @@ suite('ExtensionsMerge', () => { aLocalSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const remoteExtensions = [ - anSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), - anSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), + aSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), + aSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), ]; const expected = [ anExpectedSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -133,8 +133,8 @@ suite('ExtensionsMerge', () => { aLocalSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const remoteExtensions = [ - anSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), - anSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), + aSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), + aSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), ]; const expected = [ anExpectedSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -155,16 +155,16 @@ suite('ExtensionsMerge', () => { test('merge local and remote extensions when remote is moved forwarded', () => { const baseExtensions = [ - anSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), - anSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), + aSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), + aSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const localExtensions = [ aLocalSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), aLocalSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const remoteExtensions = [ - anSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), - anSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), + aSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), + aSyncExtension({ identifier: { id: 'c', uuid: 'c' } }), ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], [], []); @@ -238,7 +238,7 @@ suite('ExtensionsMerge', () => { aLocalSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const skippedExtensions = [ - anSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), + aSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), ]; const remoteExtensions = [ aRemoteSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -265,7 +265,7 @@ suite('ExtensionsMerge', () => { aLocalSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const skippedExtensions = [ - anSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), + aSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), ]; const remoteExtensions = [ aRemoteSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -364,7 +364,7 @@ suite('ExtensionsMerge', () => { aRemoteSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const skippedExtensions = [ - anSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), + aSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const localExtensions = [ aLocalSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -394,7 +394,7 @@ suite('ExtensionsMerge', () => { aRemoteSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const skippedExtensions = [ - anSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), + aSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const localExtensions = [ aLocalSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -481,7 +481,7 @@ suite('ExtensionsMerge', () => { aRemoteSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const skippedExtensions = [ - anSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), + aSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), ]; const localExtensions = [ aLocalSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -512,7 +512,7 @@ suite('ExtensionsMerge', () => { aRemoteSyncExtension({ identifier: { id: 'd', uuid: 'd' } }), ]; const skippedExtensions = [ - anSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), + aSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), ]; const localExtensions = [ aLocalSyncExtension({ identifier: { id: 'b', uuid: 'b' } }), @@ -1317,6 +1317,74 @@ suite('ExtensionsMerge', () => { assert.deepStrictEqual(actual.remote, null); }); + test('sync adding local application scoped extension', () => { + const localExtensions = [ + aLocalSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: true }), + ]; + + const actual = merge(localExtensions, null, null, [], [], []); + + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, localExtensions); + }); + + test('sync merging local extension with isApplicationScoped property and remote does not has isApplicationScoped property', () => { + const localExtensions = [ + aLocalSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: false }), + ]; + + const baseExtensions = [ + aSyncExtension({ identifier: { id: 'a', uuid: 'a' } }), + ]; + + const actual = merge(localExtensions, baseExtensions, baseExtensions, [], [], []); + + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, [anExpectedSyncExtension({ identifier: { id: 'a', uuid: 'a' } })]); + }); + + test('sync merging when applicaiton scope is changed locally', () => { + const localExtensions = [ + aLocalSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: true }), + ]; + + const baseExtensions = [ + aRemoteSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: false }), + ]; + + const actual = merge(localExtensions, baseExtensions, baseExtensions, [], [], []); + + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, []); + assert.deepStrictEqual(actual.remote?.all, localExtensions); + }); + + test('sync merging when applicaiton scope is changed remotely', () => { + const localExtensions = [ + aLocalSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: false }), + ]; + + const baseExtensions = [ + aRemoteSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: false }), + ]; + + const remoteExtensions = [ + aRemoteSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: true }), + ]; + + const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], [], []); + + assert.deepStrictEqual(actual.local.added, []); + assert.deepStrictEqual(actual.local.removed, []); + assert.deepStrictEqual(actual.local.updated, [anExpectedSyncExtension({ identifier: { id: 'a', uuid: 'a' }, isApplicationScoped: true })]); + assert.deepStrictEqual(actual.remote, null); + }); + function anExpectedSyncExtension(extension: Partial): ISyncExtension { return { identifier: { id: 'a', uuid: 'a' }, @@ -1360,7 +1428,7 @@ suite('ExtensionsMerge', () => { }; } - function anSyncExtension(extension: Partial): ISyncExtension { + function aSyncExtension(extension: Partial): ISyncExtension { return { identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', diff --git a/src/vs/platform/window/electron-sandbox/window.ts b/src/vs/platform/window/electron-sandbox/window.ts index 7ffa0edf69cef..90f1371bdeee3 100644 --- a/src/vs/platform/window/electron-sandbox/window.ts +++ b/src/vs/platform/window/electron-sandbox/window.ts @@ -14,10 +14,7 @@ import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; export function applyZoom(zoomLevel: number): void { webFrame.setZoomLevel(zoomLevel); setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); - // Cannot be trusted because the webFrame might take some time - // until it really applies the new zoom level - // See https://github.com/microsoft/vscode/issues/26151 - setZoomLevel(zoomLevel, false /* isTrusted */); + setZoomLevel(zoomLevel); } export function zoomIn(): void { diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index e252df07d6761..19f04231d8eae 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ExtHostChatShape, ExtHostContext, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatShape, ExtHostContext, IChatRequestDto, IChatResponseProgressDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { IChat, IChatDynamicRequest, IChatProgress, IChatRequest, IChatResponse, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -16,11 +17,14 @@ import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/ext export class MainThreadChat extends Disposable implements MainThreadChatShape { private readonly _providerRegistrations = this._register(new DisposableMap()); - private readonly _activeRequestProgressCallbacks = new Map void>(); + private readonly _activeRequestProgressCallbacks = new Map (DeferredPromise | void)>(); private readonly _stateEmitters = new Map>(); private readonly _proxy: ExtHostChatShape; + private _responsePartHandlePool = 0; + private readonly _activeResponsePartPromises = new Map>(); + constructor( extHostContext: IExtHostContext, @IChatService private readonly _chatService: IChatService, @@ -54,7 +58,14 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { } $transferChatSession(sessionId: number, toWorkspace: UriComponents): void { - this._chatService.transferChatSession(sessionId, URI.revive(toWorkspace)); + const sessionIdStr = this._chatService.getSessionId(sessionId); + if (!sessionIdStr) { + throw new Error(`Failed to transfer session. Unknown session provider ID: ${sessionId}`); + } + + const widget = this._chatWidgetService.getWidgetBySessionId(sessionIdStr); + const inputValue = widget?.inputEditor.getValue() ?? ''; + this._chatService.transferChatSession({ sessionId: sessionIdStr, inputValue: inputValue }, URI.revive(toWorkspace)); } async $registerChatProvider(handle: number, id: string): Promise { @@ -133,8 +144,25 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { this._providerRegistrations.set(handle, unreg); } - $acceptResponseProgress(handle: number, sessionId: number, progress: IChatProgress): void { + async $acceptResponseProgress(handle: number, sessionId: number, progress: IChatResponseProgressDto, responsePartHandle?: number): Promise { const id = `${handle}_${sessionId}`; + + if ('placeholder' in progress) { + const responsePartId = `${id}_${++this._responsePartHandlePool}`; + const deferredContentPromise = new DeferredPromise(); + this._activeResponsePartPromises.set(responsePartId, deferredContentPromise); + this._activeRequestProgressCallbacks.get(id)?.({ ...progress, resolvedContent: deferredContentPromise.p }); + return this._responsePartHandlePool; + } else if (responsePartHandle) { + // Complete an existing deferred promise with resolved content + const responsePartId = `${id}_${responsePartHandle}`; + const deferredContentPromise = this._activeResponsePartPromises.get(responsePartId); + if (deferredContentPromise && 'content' in progress) { + deferredContentPromise.complete(progress.content); + this._activeResponsePartPromises.delete(responsePartId); + } + } + this._activeRequestProgressCallbacks.get(id)?.(progress); } diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 1ef0daed53e89..4d28ce7f8e9e3 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -21,6 +21,7 @@ import { Emitter } from 'vs/base/common/event'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ResourceMap } from 'vs/base/common/map'; import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { ErrorNoTelemetry } from 'vs/base/common/errors'; export class BoundModelReferenceCollection { @@ -212,7 +213,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen async $tryOpenDocument(uriData: UriComponents): Promise { const inputUri = URI.revive(uriData); if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) { - throw new Error(`Invalid uri. Scheme and authority or path must be set.`); + throw new ErrorNoTelemetry(`Invalid uri. Scheme and authority or path must be set.`); } const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri); @@ -232,14 +233,14 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen try { documentUri = await promise; } catch (err) { - throw new Error(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`); + throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`); } if (!documentUri) { - throw new Error(`cannot open ${canonicalUri.toString()}`); + throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}`); } else if (!extUri.isEqual(documentUri, canonicalUri)) { - throw new Error(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`); + throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`); } else if (!this._modelTrackers.has(canonicalUri)) { - throw new Error(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`); + throw new ErrorNoTelemetry(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`); } else { return canonicalUri; } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index b2f7c9a7dd300..ffa8985978f8b 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -85,6 +85,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._store.add(_terminalService.onDidChangeActiveInstance(instance => this._onActiveTerminalChanged(instance ? instance.instanceId : null))); this._store.add(_terminalService.onDidChangeInstanceTitle(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); this._store.add(_terminalService.onDidInputInstanceData(instance => this._proxy.$acceptTerminalInteraction(instance.instanceId))); + this._store.add(_terminalService.onDidChangeSelection(instance => this._proxy.$acceptTerminalSelection(instance.instanceId, instance.selection))); // Set initial ext host state for (const instance of this._terminalService.instances) { diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 04170a3c9a06b..279a4f05c2859 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -18,6 +18,7 @@ import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dat import { VSBuffer } from 'vs/base/common/buffer'; import { DataTransferFileCache } from 'vs/workbench/api/common/shared/dataTransferCache'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -90,8 +91,8 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie return Promise.resolve(); } - $setMessage(treeViewId: string, message: string): void { - this.logService.trace('MainThreadTreeViews#$setMessage', treeViewId, message); + $setMessage(treeViewId: string, message: string | IMarkdownString): void { + this.logService.trace('MainThreadTreeViews#$setMessage', treeViewId, message.toString()); const viewer = this.getTreeView(treeViewId); if (viewer) { diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index b58a0accfffe2..f2633afa04525 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -18,6 +18,8 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { CancellationToken } from 'vs/base/common/cancellation'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { forwardedPortsViewEnabled } from 'vs/workbench/contrib/remote/browser/tunnelView'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape, PortAttributesProvider { @@ -32,7 +34,8 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun @INotificationService private readonly notificationService: INotificationService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILogService private readonly logService: ILogService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService); @@ -192,6 +195,8 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun if (features) { this.tunnelService.setTunnelFeatures(features); } + // At this point we clearly want the ports view/features since we have a tunnel factory + this.contextKeyService.createKey(forwardedPortsViewEnabled.key, true); } async $setCandidateFilter(): Promise { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 1e585c0d3b01f..653d447ef1374 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1088,6 +1088,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'portsAttributes'); return extHostTunnelService.registerPortsAttributesProvider(portSelector, provider); }, + registerTunnelProvider: (tunnelProvider: vscode.TunnelProvider, information: vscode.TunnelInformation) => { + checkProposedApiEnabled(extension, 'tunnelFactory'); + return extHostTunnelService.registerTunnelProvider(tunnelProvider, information); + }, registerTimelineProvider: (scheme: string | string[], provider: vscode.TimelineProvider) => { checkProposedApiEnabled(extension, 'timeline'); return extHostTimeline.registerTimelineProvider(scheme, provider, extension.identifier, extHostCommands.converter); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a9dabecd9dec0..ec414fd926973 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ 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 { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; -import { IChatProgress, IChatResponseErrorDetails, IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatResponseErrorDetails, IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -279,7 +279,7 @@ export interface MainThreadTreeViewsShape extends IDisposable { $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean; manuallyManageCheckboxes: boolean }): Promise; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise; $reveal(treeViewId: string, itemInfo: { item: ITreeItem; parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise; - $setMessage(treeViewId: string, message: string): void; + $setMessage(treeViewId: string, message: string | IMarkdownString): void; $setTitle(treeViewId: string, title: string, description: string | undefined): void; $setBadge(treeViewId: string, badge: IViewBadge | undefined): void; $resolveDropFileData(destinationViewId: string, requestId: number, dataItemId: string): Promise; @@ -1166,13 +1166,15 @@ export interface IChatResponseDto { }; } +export type IChatResponseProgressDto = { content: string } | { requestId: string } | { placeholder: string }; + export interface MainThreadChatShape extends IDisposable { $registerChatProvider(handle: number, id: string): Promise; $acceptChatState(sessionId: number, state: any): Promise; $addRequest(context: any): void; $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): void; $unregisterChatProvider(handle: number): Promise; - $acceptResponseProgress(handle: number, sessionId: number, progress: IChatProgress): void; + $acceptResponseProgress(handle: number, sessionId: number, progress: IChatResponseProgressDto, responsePartHandle?: number): Promise; $transferChatSession(sessionId: number, toWorkspace: UriComponents): void; $registerSlashCommandProvider(handle: number, chatProviderId: string): Promise; @@ -2012,6 +2014,7 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalDimensions(id: number, cols: number, rows: number): void; $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; $acceptTerminalInteraction(id: number): void; + $acceptTerminalSelection(id: number, selection: string | undefined): void; $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise; $acceptProcessAckDataEvent(id: number, charCount: number): void; $acceptProcessInput(id: number, data: string): void; diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index 70ccff3a6cd33..6a4dd119dc9f4 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -3,6 +3,7 @@ * 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 { Emitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; @@ -14,7 +15,7 @@ import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/exte import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostChatShape, IChatRequestDto, IChatResponseDto, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { IChatFollowup, IChatProgress, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import type * as vscode from 'vscode'; class ChatProviderWrapper { @@ -214,8 +215,20 @@ export class ExtHostChat implements ExtHostChatShape { firstProgress = stopWatch.elapsed(); } - const vscodeProgress: IChatProgress = 'responseId' in progress ? { requestId: progress.responseId } : progress; - this._proxy.$acceptResponseProgress(handle, sessionId, vscodeProgress); + if ('responseId' in progress) { + this._proxy.$acceptResponseProgress(handle, sessionId, { requestId: progress.responseId }); + } else if ('placeholder' in progress && 'resolvedContent' in progress) { + const resolvedContent = Promise.all([this._proxy.$acceptResponseProgress(handle, sessionId, { placeholder: progress.placeholder }), progress.resolvedContent]); + raceCancellation(resolvedContent, token).then((res) => { + if (!res) { + return; /* Cancelled */ + } + const [progressHandle, progressContent] = res; + this._proxy.$acceptResponseProgress(handle, sessionId, progressContent, progressHandle ?? undefined); + }); + } else { + this._proxy.$acceptResponseProgress(handle, sessionId, progress); + } } }; let result: vscode.InteractiveResponseForProgress | undefined | null; diff --git a/src/vs/workbench/api/common/extHostInlineChat.ts b/src/vs/workbench/api/common/extHostInlineChat.ts index 866af7a087d21..5d958e7d659b3 100644 --- a/src/vs/workbench/api/common/extHostInlineChat.ts +++ b/src/vs/workbench/api/common/extHostInlineChat.ts @@ -234,6 +234,9 @@ export class ExtHostInteractiveEditor implements ExtHostInlineChatShape { case InlineChatResponseFeedbackKind.Undone: apiKind = extHostTypes.InteractiveEditorResponseFeedbackKind.Undone; break; + case InlineChatResponseFeedbackKind.Accepted: + apiKind = extHostTypes.InteractiveEditorResponseFeedbackKind.Accepted; + break; } entry.provider.handleInteractiveEditorResponseFeedback?.(sessionData.session, response, apiKind); diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index a6c7fd37e18b4..96e5d9671353b 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -75,6 +75,7 @@ export class ExtHostTerminal { private _rows: number | undefined; private _exitStatus: vscode.TerminalExitStatus | undefined; private _state: vscode.TerminalState = { isInteractedWith: false }; + private _selection: string | undefined; public isOpen: boolean = false; @@ -106,6 +107,9 @@ export class ExtHostTerminal { get state(): vscode.TerminalState { return that._state; }, + get selection(): string | undefined { + return that._selection; + }, sendText(text: string, addNewLine: boolean = true): void { that._checkDisposed(); that._proxy.$sendText(that._id, text, addNewLine); @@ -233,6 +237,10 @@ export class ExtHostTerminal { return false; } + public setSelection(selection: string | undefined): void { + this._selection = selection; + } + public _setProcessId(processId: number | undefined): void { // The event may fire 2 times when the panel is restored if (this._pidPromiseComplete) { @@ -615,6 +623,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } } + public $acceptTerminalSelection(id: number, selection: string | undefined): void { + this._getTerminalById(id)?.setSelection(selection); + } + public $acceptProcessResize(id: number, cols: number, rows: number): void { try { this._terminalProcesses.get(id)?.resize(cols, rows); diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 3aaa6a38b29c0..3f33b7d7b62a0 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -20,7 +20,7 @@ import { equals, coalesce } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { MarkdownString, ViewBadge, DataTransfer } from 'vs/workbench/api/common/extHostTypeConverters'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { ITreeViewsDnDService, TreeViewsDnDService } from 'vs/editor/common/services/treeViewsDnd'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; @@ -95,7 +95,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { const treeView = this.createExtHostTreeView(viewId, options, extension); const proxyOptions = { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, dropMimeTypes, dragMimeTypes, hasHandleDrag, hasHandleDrop, manuallyManageCheckboxes: !!options.manageCheckboxStateManually }; const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, proxyOptions); - return { + const view = { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, get onDidExpandElement() { return treeView.onDidExpandElement; }, get selection() { return treeView.selectedElements; }, @@ -114,7 +114,10 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.onDidChangeCheckboxState; }, get message() { return treeView.message; }, - set message(message: string) { + set message(message: string | vscode.MarkdownString) { + if (isMarkdownString(message)) { + checkProposedApiEnabled(extension, 'treeViewMarkdownMessage'); + } treeView.message = message; }, get title() { return treeView.title; }, @@ -150,6 +153,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { treeView.dispose(); } }; + return view as vscode.TreeView; } $getChildren(treeViewId: string, treeItemHandle?: string): Promise { @@ -382,7 +386,7 @@ class ExtHostTreeView extends Disposable { }); } if (message) { - this.proxy.$setMessage(this.viewId, this._message); + this.proxy.$setMessage(this.viewId, MarkdownString.fromStrict(this._message) ?? ''); } })); } @@ -427,12 +431,12 @@ class ExtHostTreeView extends Disposable { } } - private _message: string = ''; - get message(): string { + private _message: string | vscode.MarkdownString = ''; + get message(): string | vscode.MarkdownString { return this._message; } - set message(message: string) { + set message(message: string | vscode.MarkdownString) { this._message = message; this._onDidChangeData.fire({ message: true, element: false }); } diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 4df3fc4c6005f..fbebe3ba9ed6d 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; @@ -55,6 +56,7 @@ export interface IExtHostTunnelService extends ExtHostTunnelServiceShape { onDidChangeTunnels: vscode.Event; setTunnelFactory(provider: vscode.RemoteAuthorityResolver | undefined): Promise; registerPortsAttributesProvider(portSelector: PortAttributesSelector, provider: vscode.PortAttributesProvider): IDisposable; + registerTunnelProvider(provider: vscode.TunnelProvider, information: vscode.TunnelInformation): Promise; } export const IExtHostTunnelService = createDecorator('IExtHostTunnelService'); @@ -62,7 +64,7 @@ export const IExtHostTunnelService = createDecorator('IEx export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { readonly _serviceBrand: undefined; protected readonly _proxy: MainThreadTunnelServiceShape; - private _forwardPortProvider: ((tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => Thenable | undefined) | undefined; + private _forwardPortProvider: ((tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions, token?: vscode.CancellationToken) => Thenable | undefined) | undefined; private _showCandidatePort: (host: string, port: number, detail: string) => Thenable = () => { return Promise.resolve(true); }; private _extensionTunnels: Map> = new Map(); private _onDidChangeTunnels: Emitter = new Emitter(); @@ -135,6 +137,27 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe async $registerCandidateFinder(_enable: boolean): Promise { } + registerTunnelProvider(provider: vscode.TunnelProvider, information: vscode.TunnelInformation): Promise { + if (this._forwardPortProvider) { + 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); + return result ?? undefined; + }; + + const tunnelFeatures = information.tunnelFeatures ? { + elevation: !!information.tunnelFeatures?.elevation, + privacyOptions: information.tunnelFeatures?.privacyOptions + } : undefined; + + this._proxy.$setTunnelProvider(tunnelFeatures); + return Promise.resolve(toDisposable(() => { + this._forwardPortProvider = undefined; + this._proxy.$setTunnelProvider(undefined); + })); + } + async setTunnelFactory(provider: vscode.RemoteAuthorityResolver | undefined): Promise { // Do not wait for any of the proxy promises here. // It will delay startup and there is nothing that needs to be waited for. @@ -201,11 +224,15 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (this._forwardPortProvider) { try { this.logService.trace('ForwardedPorts: (ExtHostTunnelService) Getting tunnel from provider.'); - const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions); + const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions,); this.logService.trace('ForwardedPorts: (ExtHostTunnelService) Got tunnel promise from provider.'); if (providedPort !== undefined) { const tunnel = await providedPort; this.logService.trace('ForwardedPorts: (ExtHostTunnelService) Successfully awaited tunnel from provider.'); + if (tunnel === undefined) { + this.logService.error('ForwardedPorts: (ExtHostTunnelService) Resolved tunnel is undefined'); + return undefined; + } if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) { this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map()); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 2647d41c5db3e..057aee98ad494 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4092,7 +4092,8 @@ export enum InteractiveSessionCopyKind { export enum InteractiveEditorResponseFeedbackKind { Unhelpful = 0, Helpful = 1, - Undone = 2 + Undone = 2, + Accepted = 3 } //#endregion diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 133cffc590035..c78016251f280 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -18,6 +18,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch } from '@vscode/proxy-agent'; +const systemCertificatesV2Default = true; + export function connectProxyResolver( extHostWorkspace: IExtHostWorkspaceProvider, configProvider: ExtHostConfigProvider, @@ -30,7 +32,11 @@ export function connectProxyResolver( const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; const params: ProxyAgentParams = { resolveProxy: url => extHostWorkspace.resolveProxy(url), - getHttpProxySetting: () => configProvider.getConfiguration('http').get('proxy'), + lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostLogService, mainThreadTelemetry, configProvider, {}, initData.remote.isRemote), + getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), + getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', + getSystemCertificatesV1: () => certSettingV1(configProvider), + getSystemCertificatesV2: () => certSettingV2(configProvider), log: (level, message, ...args) => { switch (level) { case LogLevel.Trace: extHostLogService.trace(message, ...args); break; @@ -51,49 +57,18 @@ export function connectProxyResolver( // TODO @chrmarti Remove this from proxy agent proxyResolveTelemetry: () => { }, useHostProxy: doUseHostProxy, - useSystemCertificatesV2: certSettingV2(configProvider), addCertificates: [], env: process.env, }; - configProvider.onDidChangeConfiguration(e => { - params.useSystemCertificatesV2 = certSettingV2(configProvider); - }); const resolveProxy = createProxyResolver(params); - const lookup = createPatchedModules(params, configProvider, resolveProxy); + const lookup = createPatchedModules(params, resolveProxy); return configureModuleLoading(extensionService, lookup); } -function createPatchedModules(params: ProxyAgentParams, configProvider: ExtHostConfigProvider, resolveProxy: ReturnType) { - const proxySetting = { - config: configProvider.getConfiguration('http') - .get('proxySupport') || 'off' - }; - configProvider.onDidChangeConfiguration(e => { - proxySetting.config = configProvider.getConfiguration('http') - .get('proxySupport') || 'off'; - }); - const certSetting = { - config: certSettingV1(configProvider) - }; - configProvider.onDidChangeConfiguration(e => { - certSetting.config = certSettingV1(configProvider); - }); - +function createPatchedModules(params: ProxyAgentParams, resolveProxy: ReturnType) { return { - http: { - off: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'off' }, certSetting, true)), - on: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'on' }, certSetting, true)), - override: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'override' }, certSetting, true)), - onRequest: Object.assign({}, http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, true)), - default: Object.assign(http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, false)) // run last - } as Record, - https: { - off: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'off' }, certSetting, true)), - on: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'on' }, certSetting, true)), - override: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'override' }, certSetting, true)), - onRequest: Object.assign({}, https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, true)), - default: Object.assign(https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, false)) // run last - } as Record, + http: Object.assign(http, createHttpPatch(params, http, resolveProxy)), + https: Object.assign(https, createHttpPatch(params, https, resolveProxy)), net: Object.assign(net, createNetPatch(params, net)), tls: Object.assign(tls, createTlsPatch(params, tls)) }; @@ -101,12 +76,12 @@ function createPatchedModules(params: ProxyAgentParams, configProvider: ExtHostC function certSettingV1(configProvider: ExtHostConfigProvider) { const http = configProvider.getConfiguration('http'); - return !http.get('experimental.systemCertificatesV2') && !!http.get('systemCertificates'); + return !http.get('experimental.systemCertificatesV2', systemCertificatesV2Default) && !!http.get('systemCertificates'); } function certSettingV2(configProvider: ExtHostConfigProvider) { const http = configProvider.getConfiguration('http'); - return !!http.get('experimental.systemCertificatesV2') && !!http.get('systemCertificates'); + return !!http.get('experimental.systemCertificatesV2', systemCertificatesV2Default) && !!http.get('systemCertificates'); } const modulesCache = new Map(); @@ -128,17 +103,78 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku return original.apply(this, arguments); } - const modules = lookup[request]; const ext = extensionPaths.findSubstr(URI.file(parent.filename)); let cache = modulesCache.get(ext); if (!cache) { modulesCache.set(ext, cache = {}); } if (!cache[request]) { - const mod = modules.default; + const mod = lookup[request]; cache[request] = { ...mod }; // Copy to work around #93167. } return cache[request]; }; }); } + +async function lookupProxyAuthorization( + extHostLogService: ILogService, + mainThreadTelemetry: MainThreadTelemetryShape, + configProvider: ExtHostConfigProvider, + proxyAuthenticateCache: Record, + isRemote: boolean, + proxyURL: string, + proxyAuthenticate: string | string[] | undefined, + state: { kerberosRequested?: boolean } +): Promise { + const cached = proxyAuthenticateCache[proxyURL]; + if (proxyAuthenticate) { + proxyAuthenticateCache[proxyURL] = proxyAuthenticate; + } + extHostLogService.trace('ProxyResolver#lookupProxyAuthorization callback', `proxyURL:${proxyURL}`, `proxyAuthenticate:${proxyAuthenticate}`, `proxyAuthenticateCache:${cached}`); + const header = proxyAuthenticate || cached; + const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : []; + sendTelemetry(mainThreadTelemetry, authenticate, isRemote); + if (authenticate.some(a => /^(Negotiate|Kerberos)( |$)/i.test(a)) && !state.kerberosRequested) { + try { + state.kerberosRequested = true; + const kerberos = await import('kerberos'); + const url = new URL(proxyURL); + const spn = configProvider.getConfiguration('http').get('proxyKerberosServicePrincipal') + || (process.platform === 'win32' ? `HTTP/${url.hostname}` : `HTTP@${url.hostname}`); + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup', `proxyURL:${proxyURL}`, `spn:${spn}`); + const client = await kerberos.initializeClient(spn); + const response = await client.step(''); + return 'Negotiate ' + response; + } catch (err) { + extHostLogService.error('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); + } + } + return undefined; +} + +type ProxyAuthenticationClassification = { + owner: 'chrmarti'; + comment: 'Data about proxy authentication requests'; + authenticationType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Type of the authentication requested' }; + extensionHostType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of the extension host' }; +}; + +type ProxyAuthenticationEvent = { + authenticationType: string; + extensionHostType: string; +}; + +let telemetrySent = false; + +function sendTelemetry(mainThreadTelemetry: MainThreadTelemetryShape, authenticate: string[], isRemote: boolean) { + if (telemetrySent || !authenticate.length) { + return; + } + telemetrySent = true; + + mainThreadTelemetry.$publicLog2('proxyAuthenticationRequest', { + authenticationType: authenticate.map(a => a.split(' ')[0]).join(','), + extensionHostType: isRemote ? 'remote' : 'local', + }); +} diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts similarity index 100% rename from src/vs/workbench/api/test/browser/extHostAuthentication.test.ts rename to src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index f8722e3d06d85..c52e15228ca14 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -14,6 +14,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { isNative } from 'vs/base/common/platform'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export class TextInputActionsProvider extends Disposable implements IWorkbenchContribution { @@ -90,8 +91,10 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo EventHelper.stop(e, true); + const event = new StandardMouseEvent(e); + this.contextMenuService.showContextMenu({ - getAnchor: () => e, + getAnchor: () => event, getActions: () => this.textInputActions, getActionsContext: () => target, }); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index d09cfa07930cd..70b39c12b83a3 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -149,13 +149,9 @@ abstract class AbstractGlobalActivityActionViewItem extends ActivityActionViewIt const actions = await this.resolveContextMenuActions(disposables); const event = new StandardMouseEvent(e); - const anchor = { - x: event.posx, - y: event.posy - }; this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => event, getActions: () => actions, onHide: () => disposables.dispose() }); diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 366941297647c..afa384aa88ca0 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -656,7 +656,7 @@ export class CompositeBar extends Widget implements ICompositeBar { const event = new StandardMouseEvent(e); this.contextMenuService.showContextMenu({ - getAnchor: () => { return { x: event.posx, y: event.posy }; }, + getAnchor: () => event, getActions: () => this.getContextMenuActions(e) }); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 740e037f6d408..898bd528d2468 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -374,10 +374,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Find target anchor - let anchor: HTMLElement | { x: number; y: number } = this.element; + let anchor: HTMLElement | StandardMouseEvent = this.element; if (e instanceof MouseEvent) { - const event = new StandardMouseEvent(e); - anchor = { x: event.posx, y: event.posy }; + anchor = new StandardMouseEvent(e); } // Show it diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index ecb998d62740a..3b2fc2ad97073 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -454,10 +454,9 @@ export class TabsTitleControl extends TitleControl { EventHelper.stop(e); // Find target anchor - let anchor: HTMLElement | { x: number; y: number } = tabsContainer; + let anchor: HTMLElement | StandardMouseEvent = tabsContainer; if (e instanceof MouseEvent) { - const event = new StandardMouseEvent(e); - anchor = { x: event.posx, y: event.posy }; + anchor = new StandardMouseEvent(e); } // Show it diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 5d77c66dfc5e0..ab517fcd4bebe 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -379,10 +379,9 @@ export abstract class TitleControl extends Themable { applyAvailableEditorIds(this.editorAvailableEditorIds, editor, this.editorResolverService); // Find target anchor - let anchor: HTMLElement | { x: number; y: number } = node; + let anchor: HTMLElement | StandardMouseEvent = node; if (e instanceof MouseEvent) { - const event = new StandardMouseEvent(e); - anchor = { x: event.posx, y: event.posy }; + anchor = new StandardMouseEvent(e); } // Show it diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 62d27bfc8ecae..aa936423b461c 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -25,6 +25,8 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IntervalCounter } from 'vs/base/common/async'; import { assertIsDefined } from 'vs/base/common/types'; import { NotificationsToastsVisibleContext } from 'vs/workbench/common/contextkeys'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; interface INotificationToast { readonly item: INotificationViewItem; @@ -83,7 +85,9 @@ export class NotificationsToasts extends Themable implements INotificationsToast @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IHostService private readonly hostService: IHostService + @IHostService private readonly hostService: IHostService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { super(themeService); @@ -187,11 +191,24 @@ export class NotificationsToasts extends Themable implements INotificationsToast const notificationList = this.instantiationService.createInstance(NotificationsList, notificationToast, { verticalScrollMode: ScrollbarVisibility.Hidden, widgetAriaLabel: (() => { - if (!item.source) { - return localize('notificationAriaLabel', "{0}, notification", item.message.raw); + let accessibleViewHint: string | undefined; + const keybinding = this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); + if (this._configurationService.getValue('accessibility.verbosity.notification')) { + accessibleViewHint = keybinding ? localize('chatAccessibleViewHint', "Inspect the response in the accessible view with {0}", keybinding) : localize('chatAccessibleViewHintNoKb', "Inspect the response in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding"); } - return localize('notificationWithSourceAriaLabel', "{0}, source: {1}, notification", item.message.raw, item.source); + if (!item.source) { + if (accessibleViewHint) { + return localize('notificationAriaLabelViewHint', "{0}, notification {1}", item.message.raw, accessibleViewHint); + } else { + return localize('notificationAriaLabel', "{0}, notification", item.message.raw); + } + } + if (accessibleViewHint) { + return localize('notificationWithSourceAriaLabelViewHint', "{0}, source: {1}, notification {2}", item.message.raw, item.source, accessibleViewHint); + } else { + return localize('notificationWithSourceAriaLabel', "{0}, source: {1}, notification", item.message.raw, item.source); + } })() }); itemDisposables.add(notificationList); diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 67a88f8eb1784..ceaf9b73e901d 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -282,9 +282,8 @@ export class SidebarPart extends CompositePart implements IPaneCo if (activeViewlet) { const contextMenuActions = activeViewlet ? activeViewlet.getContextMenuActions() : []; if (contextMenuActions.length) { - const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => event, getActions: () => contextMenuActions.slice(), getActionViewItem: action => this.actionViewItemProvider(action), actionRunner: activeViewlet.getActionRunner(), diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 66e1c66e166e5..1c949d44a0056 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -453,7 +453,7 @@ export class StatusbarPart extends Part implements IStatusbarService { let actions: IAction[] | undefined = undefined; this.contextMenuService.showContextMenu({ - getAnchor: () => ({ x: event.posx, y: event.posy }), + getAnchor: () => event, getActions: () => { actions = this.getContextMenuActions(event); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 64a6afb239d5e..9211cd3decf33 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -399,11 +399,10 @@ export class TitlebarPart extends Part implements ITitleService { protected onContextMenu(e: MouseEvent, menuId: MenuId): void { // Find target anchor const event = new StandardMouseEvent(e); - const anchor = { x: event.posx, y: event.posy }; // Show it this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => event, menuId, contextKeyService: this.contextKeyService, domForShadowRoot: isMacintosh && isNative ? event.target : undefined diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 80b65021a1020..05fb4fc8c93eb 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -46,6 +46,10 @@ padding-left: 24px; } +.monaco-workbench .tree-explorer-viewlet-tree-view .message a { + color: var(--vscode-textLink-foreground); +} + .monaco-workbench .tree-explorer-viewlet-tree-view .message.hide { display: none; } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 77b9e62a7b73b..aedd476a89d9d 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -21,7 +21,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { Schemas } from 'vs/base/common/network'; @@ -72,6 +72,7 @@ import { AriaRole } from 'vs/base/browser/ui/aria/aria'; import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; +import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; export class TreeViewPane extends ViewPane { @@ -182,6 +183,10 @@ function isTreeCommandEnabled(treeCommand: TreeCommand, contextKeyService: ICont return true; } +function isRenderedMessageValue(messageValue: string | IMarkdownRenderResult | undefined): messageValue is IMarkdownRenderResult { + return !!messageValue && typeof messageValue !== 'string' && 'element' in messageValue && 'dispose' in messageValue; +} + const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); export const RawCustomTreeViewContextKey = new RawContextKey('customTreeView', false); @@ -204,7 +209,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private focused: boolean = false; private domNode!: HTMLElement; private treeContainer: HTMLElement | undefined; - private _messageValue: string | undefined; + private _messageValue: string | { element: HTMLElement; dispose: () => void } | undefined; private _canSelectMany: boolean = false; private _manuallyManageCheckboxes: boolean = false; private messageElement: HTMLElement | undefined; @@ -214,6 +219,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private _container: HTMLElement | undefined; private root: ITreeItem; + private markdownRenderer: MarkdownRenderer | undefined; private elementsToRefresh: ITreeItem[] = []; private lastSelection: readonly ITreeItem[] = []; private lastActive: ITreeItem; @@ -388,12 +394,12 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this._onDidChangeWelcomeState.fire(); } - private _message: string | undefined; - get message(): string | undefined { + private _message: string | IMarkdownString | undefined; + get message(): string | IMarkdownString | undefined { return this._message; } - set message(message: string | undefined) { + set message(message: string | IMarkdownString | undefined) { this._message = message; this.updateMessage(); this._onDidChangeWelcomeState.fire(); @@ -825,15 +831,23 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this.updateContentAreas(); } - private showMessage(message: string): void { - this._messageValue = message; + private showMessage(message: string | IMarkdownString): void { + if (isRenderedMessageValue(this._messageValue)) { + this._messageValue.dispose(); + } + if (isMarkdownString(message) && !this.markdownRenderer) { + this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); + } + this._messageValue = isMarkdownString(message) ? this.markdownRenderer!.render(message) : message; if (!this.messageElement) { return; } this.messageElement.classList.remove('hide'); this.resetMessageElement(); - if (!isFalsyOrWhitespace(this._message)) { + if (typeof this._messageValue === 'string' && !isFalsyOrWhitespace(this._messageValue)) { this.messageElement.textContent = this._messageValue; + } else if (isRenderedMessageValue(this._messageValue)) { + this.messageElement.appendChild(this._messageValue.element); } this.layout(this._height, this._width); } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index ff766b9acb815..1f593cd65c3ad 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -577,9 +577,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { event.stopPropagation(); event.preventDefault(); - const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => event, getActions: () => this.menuActions?.getContextMenuActions() ?? [] }); } @@ -743,9 +742,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const actions: IAction[] = viewPane.menuActions.getContextMenuActions(); - const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => event, getActions: () => actions }); } diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index b5b325c51ef07..db5aa59f80eda 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -26,6 +26,7 @@ import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/envir import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { BrowserLifecycleService } from 'vs/workbench/services/lifecycle/browser/lifecycleService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; export class BrowserWindow extends Disposable { @@ -37,7 +38,8 @@ export class BrowserWindow extends Disposable { @IProductService private readonly productService: IProductService, @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IHostService private readonly hostService: IHostService ) { super(); @@ -231,13 +233,15 @@ export class BrowserWindow extends Disposable { ); } - await this.dialogService.prompt({ + // While this dialog shows, closing the tab will not display a confirmation dialog + // to avoid showing the user two dialogs at once + await this.hostService.withExpectedShutdown(() => this.dialogService.prompt({ type: Severity.Info, message: localize('openExternalDialogTitle', "All done. You can close this tab now."), detail, buttons, cancelButton: true - }); + })); }; // We cannot know whether the protocol handler succeeded. diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 98d32c500de01..c88db4f5dafad 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -181,7 +181,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'enum': ['auto', 'distribute', 'split'], 'default': 'auto', 'enumDescriptions': [ - localize('workbench.editor.splitSizingAuto', "Splits all the editor groups to equal parts unless a part has been changed in size."), + localize('workbench.editor.splitSizingAuto', "Splits the active editor group to equal parts, unless all editor groups are already in equal parts. In that case, splits all the editor groups to equal parts."), localize('workbench.editor.splitSizingDistribute', "Splits all the editor groups to equal parts."), localize('workbench.editor.splitSizingSplit', "Splits the active editor group to equal parts.") ], diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index f86f9cb92bb8e..8306ee74803d4 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -653,7 +653,7 @@ export interface ITreeView extends IDisposable { manuallyManageCheckboxes: boolean; - message?: string; + message?: string | IMarkdownString; title: string; diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index 63adda5f1fa69..c500500534f8d 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -12,7 +12,7 @@ import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/b import { localize } from 'vs/nls'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibilityHelpAction, AccessibleViewAction, AccessibleViewNextAction, AccessibleViewPreviousAction, registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { AccessibilityHelpAction, AccessibilityVerbositySettingId, AccessibleViewAction, AccessibleViewNextAction, AccessibleViewPreviousAction, registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; import * as strings from 'vs/base/common/strings'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; @@ -36,7 +36,7 @@ class AccessibilityHelpProvider implements IAccessibleContentProvider { this._editor.focus(); } options: IAccessibleViewOptions = { type: AccessibleViewType.HelpMenu, ariaLabel: localize('editor-help', "editor accessibility help"), readMoreUrl: 'https://go.microsoft.com/fwlink/?linkid=851010' }; - verbositySettingKey: string = 'editor'; + verbositySettingKey = AccessibilityVerbositySettingId.Editor; constructor( private readonly _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService @@ -117,8 +117,9 @@ class HoverAccessibleViewContribution extends Disposable { if (!editorHoverContent) { return false; } + this._options.language = editor?.getModel()?.getLanguageId() ?? undefined; accessibleViewService.show({ - verbositySettingKey: 'hover', + verbositySettingKey: AccessibilityVerbositySettingId.Hover, provideContent() { return editorHoverContent; }, onClose() { }, options: this._options @@ -135,7 +136,7 @@ class HoverAccessibleViewContribution extends Disposable { return false; } accessibleViewService.show({ - verbositySettingKey: 'hover', + verbositySettingKey: AccessibilityVerbositySettingId.Hover, provideContent() { return extensionHoverContent; }, onClose() { }, options: this._options @@ -208,7 +209,7 @@ class NotificationAccessibleViewContribution extends Disposable { list.focusPrevious(); renderAccessibleView(); }, - verbositySettingKey: 'notifications', + verbositySettingKey: AccessibilityVerbositySettingId.Notification, options: { ariaLabel: localize('notification', "Notification Accessible View"), type: AccessibleViewType.View diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts index 9dc863f3efd31..80e55ef368ed2 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts @@ -17,7 +17,10 @@ export const enum AccessibilityVerbositySettingId { Chat = 'accessibility.verbosity.panelChat', InlineChat = 'accessibility.verbosity.inlineChat', KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor', - Notebook = 'accessibility.verbosity.notebook' + Notebook = 'accessibility.verbosity.notebook', + Editor = 'accessibility.verbosity.editor', + Hover = 'accessibility.verbosity.hover', + Notification = 'accessibility.verbosity.notification' } const baseProperty: object = { @@ -54,6 +57,14 @@ const configuration: IConfigurationNode = { [AccessibilityVerbositySettingId.Notebook]: { description: localize('verbosity.notebook', 'Provide information about how to focus the cell container or inner editor when a notebook cell is focused.'), ...baseProperty + }, + [AccessibilityVerbositySettingId.Hover]: { + description: localize('verbosity.hover', 'Provide information about how to open the hover in an accessible view.'), + ...baseProperty + }, + [AccessibilityVerbositySettingId.Notification]: { + description: localize('verbosity.notification', 'Provide information about how to open the notification in an accessible view.'), + ...baseProperty } } }; @@ -118,7 +129,7 @@ export const AccessibleViewNextAction = registerCommand(new MultiCommand({ menuOpts: [{ menuId: MenuId.CommandPalette, group: '', - title: localize('editor.action.accessibleViewNext', "Next Accessible View"), + title: localize('editor.action.accessibleViewNext', "Show Next in Accessible View"), order: 1 }], })); @@ -133,7 +144,7 @@ export const AccessibleViewPreviousAction = registerCommand(new MultiCommand({ menuOpts: [{ menuId: MenuId.CommandPalette, group: '', - title: localize('editor.action.accessibleViewPrevious', "Previous Accessible View"), + title: localize('editor.action.accessibleViewPrevious', "Show Previous in Accessible View"), order: 1 }], })); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 52e818673beb9..6b3971cf561dd 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -14,7 +14,6 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/wi import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; -import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -24,7 +23,9 @@ import { IInstantiationService, createDecorator } from 'vs/platform/instantiatio import { IOpenerService } from 'vs/platform/opener/common/opener'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; -import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId, AccessibleViewAction, AccessibleViewNextAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; const enum DEFAULT { WIDTH = 800, @@ -32,7 +33,7 @@ const enum DEFAULT { } export interface IAccessibleContentProvider { - verbositySettingKey: string; + verbositySettingKey: AccessibilityVerbositySettingId; provideContent(): string; onClose(): void; onKeyDown?(e: IKeyboardEvent): void; @@ -48,6 +49,11 @@ export interface IAccessibleViewService { show(provider: IAccessibleContentProvider): void; next(): void; previous(): void; + /** + * If the setting is enabled, provides the open accessible view hint as a localized string. + * @param verbositySettingKey The setting key for the verbosity of the feature + */ + getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null; } export const enum AccessibleViewType { @@ -58,6 +64,9 @@ export const enum AccessibleViewType { export interface IAccessibleViewOptions { ariaLabel: string; readMoreUrl?: string; + /** + * Defaults to markdown + */ language?: string; type: AccessibleViewType; } @@ -79,7 +88,8 @@ class AccessibleView extends Disposable { @IModelService private readonly _modelService: IModelService, @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); this._accessiblityHelpIsShown = accessibilityHelpIsShown.bindTo(this._contextKeyService); @@ -87,7 +97,7 @@ class AccessibleView extends Disposable { this._editorContainer = document.createElement('div'); this._editorContainer.classList.add('accessible-view'); const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: [...EditorExtensionsRegistry.getEditorContributions(), ...EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID, 'editor.contrib.selectionAnchorController'])] + contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID) }; const editorOptions: IEditorConstructionOptions = { ...getSimpleEditorOptions(this._configurationService), @@ -169,7 +179,7 @@ class AccessibleView extends Disposable { ? AccessibilityHelpNLS.changeConfigToOnMac : AccessibilityHelpNLS.changeConfigToOnWinLinux ); - if (accessibilitySupport && provider.verbositySettingKey === 'editor') { + if (accessibilitySupport && provider.verbositySettingKey === AccessibilityVerbositySettingId.Editor) { message = AccessibilityHelpNLS.auto_on; message += '\n'; } else if (!accessibilitySupport) { @@ -189,11 +199,9 @@ class AccessibleView extends Disposable { if (!domNode) { return; } - if (provider.options.language) { - model.setLanguage(provider.options.language); - } + model.setLanguage(provider.options.language ?? 'markdown'); container.appendChild(this._editorContainer); - this._editorWidget.updateOptions({ ariaLabel: provider.options.ariaLabel }); + this._editorWidget.updateOptions({ ariaLabel: provider.next && provider.previous ? localize('accessibleViewAriaLabelWithNav', "{0} {1}", provider.options.ariaLabel, this._getNavigationAriaHint(provider.verbositySettingKey)) : localize('accessibleViewAriaLabel', "{0}", provider.options.ariaLabel) }); this._editorWidget.focus(); }); const disposableStore = new DisposableStore(); @@ -235,6 +243,16 @@ class AccessibleView extends Disposable { } return this._modelService.createModel(resource.fragment, null, resource, false); } + + private _getNavigationAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string { + let hint = ''; + const nextKeybinding = this._keybindingService.lookupKeybinding(AccessibleViewNextAction.id)?.getAriaLabel(); + const previousKeybinding = this._keybindingService.lookupKeybinding(AccessibleViewNextAction.id)?.getAriaLabel(); + if (this._configurationService.getValue(verbositySettingKey)) { + hint = (nextKeybinding && previousKeybinding) ? localize('chatAccessibleViewNextPreviousHint', "Show the next {0} or previous {1} item in the accessible view", nextKeybinding, previousKeybinding) : localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item in the accessible view by configuring keybindings for Show Next / Previous in Accessible View"); + } + return hint; + } } export class AccessibleViewService extends Disposable implements IAccessibleViewService { @@ -242,7 +260,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView private _accessibleView: AccessibleView | undefined; constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); } @@ -259,4 +279,11 @@ export class AccessibleViewService extends Disposable implements IAccessibleView previous(): void { this._accessibleView?.previous(); } + getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { + if (!this._configurationService.getValue(verbositySettingKey)) { + return null; + } + const keybinding = this._keybindingService.lookupKeybinding(AccessibleViewAction.id)?.getAriaLabel(); + return keybinding ? localize('chatAccessibleViewHint', "Inspect this in the accessible view with {0}", keybinding) : localize('chatAccessibleViewHintNoKb', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding"); + } } diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts index 6de43d8e9a458..f34f32e9be253 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts @@ -98,15 +98,15 @@ Registry.as(ConfigurationExtensions.Configuration).regis ...audioCueFeatureBase, }, 'audioCues.diffLineInserted': { - 'description': localize('audioCues.diffLineInserted', "Plays a sound when the focus moves to an inserted line in diff review mode or to the next/previous change"), + 'description': localize('audioCues.diffLineInserted', "Plays a sound when the focus moves to an inserted line in accessible diff viewer mode or to the next/previous change"), ...audioCueFeatureBase, }, 'audioCues.diffLineDeleted': { - 'description': localize('audioCues.diffLineDeleted', "Plays a sound when the focus moves to a deleted line in diff review mode or to the next/previous change"), + 'description': localize('audioCues.diffLineDeleted', "Plays a sound when the focus moves to a deleted line in accessible diff viewer mode or to the next/previous change"), ...audioCueFeatureBase, }, 'audioCues.diffLineModified': { - 'description': localize('audioCues.diffLineModified', "Plays a sound when the focus moves to a modified line in diff review mode or to the next/previous change"), + 'description': localize('audioCues.diffLineModified', "Plays a sound when the focus moves to a modified line in accessible diff viewer mode or to the next/previous change"), ...audioCueFeatureBase, }, 'audioCues.notebookCellCompleted': { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index fd147f172eab9..bd75a451666ee 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -12,13 +12,17 @@ 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 } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { AccessibleDiffViewerNext } from 'vs/editor/browser/widget/diffEditor.contribution'; export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string { const keybindingService = accessor.get(IKeybindingService); const content = []; + const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); if (type === 'panelChat') { content.push(localize('chat.overview', 'The chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.')); content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); + content.push(openAccessibleViewKeybinding ? localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view via {0}', openAccessibleViewKeybinding) : localize('chat.inspectResponseNoKb', 'With the input box focused, inspect the last response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.')); content.push(descriptionForCommand('chat.action.focus', localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat command ({0}).',), localize('workbench.action.chat.focusNoKb', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat List command, which is currently not triggerable by a keybinding.'), keybindingService)); content.push(descriptionForCommand('workbench.action.chat.focusInput', localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command ({0})'), localize('workbench.action.interactiveSession.focusInputNoKb', 'To focus the input box for chat requests, invoke the Focus Chat Input command, which is currently not triggerable by a keybinding.'), keybindingService)); @@ -33,11 +37,11 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'pane if (upHistoryKeybinding && downHistoryKeybinding) { content.push(localize('inlineChat.requestHistory', 'In the input box, use {0} and {1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', upHistoryKeybinding, downHistoryKeybinding)); } - content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with /fix or /explain. Type / to discover more ready-made commands.")); - content.push(localize('inlineChat.fix', "When a request is prefixed with /fix, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.")); - const diffReviewKeybinding = keybindingService.lookupKeybinding('editor.action.diffReview.next')?.getAriaLabel(); + content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view via {0}', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.")); + content.push(localize('inlineChat.fix', "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.")); + const diffReviewKeybinding = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); content.push(diffReviewKeybinding ? localize('inlineChat.diff', "Once in the diff editor, enter review mode with ({0}). Use up and down arrows to navigate lines with the proposed changes.", diffReviewKeybinding) : localize('inlineChat.diffNoKb', "Tab again to enter the Diff editor with the changes and enter review mode with the Go to Next Difference Command. Use Up/DownArrow to navigate lines with the proposed changes.")); - content.push(localize('inlineChat.explain', "When a request is prefixed with /explain, a response will explain the code in the current selection and the chat view will be focused.")); content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); } content.push(localize('chat.audioCues', "Audio cues can be changed via settings with a prefix of audioCues.chat. By default, if a request takes more than 4 seconds, you will hear an audio cue indicating that progress is still occurring.")); @@ -70,7 +74,7 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi inputEditor.getSupportedActions(); const helpText = getAccessibilityHelpText(accessor, type); accessibleViewService.show({ - verbositySettingKey: type, + verbositySettingKey: type as AccessibilityVerbositySettingId, provideContent: () => helpText, onClose: () => { if (type === 'panelChat' && cachedPosition) { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 142a93a0ca761..dcbb44802fd64 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -111,7 +111,7 @@ export function registerChatActions() { return; } runAccessibilityHelpAction(accessor, codeEditor, 'panelChat'); - }, CONTEXT_IN_CHAT_INPUT)); + }, CONTEXT_IN_CHAT_SESSION)); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 10a24bd4b4b99..7f639db3aeba6 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -14,6 +14,7 @@ import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatExecuteActionContext { widget: IChatWidget; + inputValue?: string; } function isExecuteActionContext(thing: unknown): thing is IChatExecuteActionContext { @@ -48,7 +49,7 @@ export class SubmitAction extends Action2 { return; } - context.widget.acceptInput(); + context.widget.acceptInput(context.inputValue); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction.ts b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction.ts index 095b8c2df8f88..b7357eb1366ba 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction.ts @@ -22,13 +22,18 @@ import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +interface IChatQuickQuestionModeOptions { + renderInputOnTop: boolean; + useDynamicMessageLayout: boolean; +} + class BaseChatQuickQuestionMode implements IQuickQuestionMode { private _currentTimer: any | undefined; private _input: IQuickPick | undefined; private _currentChat: QuickChat | undefined; constructor( - private readonly renderInputOnTop: boolean + private readonly _options: IChatQuickQuestionModeOptions ) { } run(accessor: ServicesAccessor, query: string): void { @@ -62,12 +67,17 @@ class BaseChatQuickQuestionMode implements IQuickQuestionMode { this._input.hideInput = true; - const containerList = dom.$('.interactive-list'); - const containerSession = dom.$('.interactive-session', undefined, containerList); - containerSession.style.height = '500px'; - containerList.style.position = 'relative'; + const containerSession = dom.$('.interactive-session'); this._input.widget = containerSession; + this._currentChat ??= instantiationService.createInstance(QuickChat, { + providerId: providerInfo.id, + ...this._options + }); + // show needs to come before the current chat rendering + this._input.show(); + this._currentChat.render(containerSession); + const clearButton = { iconClass: ThemeIcon.asClassName(Codicon.clearAll), tooltip: localize('clear', "Clear"), @@ -76,7 +86,7 @@ class BaseChatQuickQuestionMode implements IQuickQuestionMode { clearButton, { iconClass: ThemeIcon.asClassName(Codicon.commentDiscussion), - tooltip: localize('openInChat', "Open in chat view"), + tooltip: localize('openInChat', "Open In Chat View"), } ]; this._input.title = providerInfo.displayName; @@ -92,12 +102,6 @@ class BaseChatQuickQuestionMode implements IQuickQuestionMode { //#endregion - this._currentChat ??= instantiationService.createInstance(QuickChat, { - providerId: providerInfo.id, - renderInputOnTop: this.renderInputOnTop, - }); - this._currentChat.render(containerSession); - disposableStore.add(this._input.onDidAccept(() => { this._currentChat?.acceptInput(); })); @@ -110,7 +114,6 @@ class BaseChatQuickQuestionMode implements IQuickQuestionMode { } })); - this._input.show(); this._currentChat.layout(); this._currentChat.focus(); @@ -134,7 +137,7 @@ class QuickChat extends Disposable { private _currentParentElement?: HTMLElement; constructor( - private readonly chatViewOptions: IChatViewOptions & { renderInputOnTop: boolean }, + private readonly _options: IChatViewOptions & IChatQuickQuestionModeOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IChatService private readonly chatService: IChatService, @@ -157,14 +160,15 @@ class QuickChat extends Disposable { } render(parent: HTMLElement): void { - this.widget?.dispose(); this._currentParentElement = parent; + this._scopedContextKeyService?.dispose(); this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + this.widget?.dispose(); this.widget = this._register( scopedInstantiationService.createInstance( ChatWidget, - { resource: true, renderInputOnTop: this.chatViewOptions.renderInputOnTop }, + { resource: true, renderInputOnTop: this._options.renderInputOnTop }, { listForeground: editorForeground, listBackground: editorBackground, @@ -173,6 +177,9 @@ class QuickChat extends Disposable { })); this.widget.render(parent); this.widget.setVisible(true); + if (this._options.useDynamicMessageLayout) { + this.widget.setDynamicChatTreeItemLayout(2, 600); + } this.updateModel(); if (this._currentQuery) { this.widget.inputEditor.setSelection({ @@ -203,7 +210,7 @@ class QuickChat extends Disposable { } async openChatView(): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(this.chatViewOptions.providerId); + const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId); if (!widget?.viewModel || !this.model) { return; } @@ -233,13 +240,13 @@ class QuickChat extends Disposable { } layout(): void { - if (this._currentParentElement) { + if (!this._options.useDynamicMessageLayout && this._currentParentElement) { this.widget.layout(500, this._currentParentElement.offsetWidth); } } private updateModel(): void { - this.model ??= this.chatService.startSession(this.chatViewOptions.providerId, CancellationToken.None); + this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None); if (!this.model) { throw new Error('Could not start chat session'); } @@ -252,7 +259,10 @@ AskQuickQuestionAction.registerMode( QuickQuestionMode.InputOnTopChat, class InputOnTopChatQuickQuestionMode extends BaseChatQuickQuestionMode { constructor() { - super(true); + super({ + renderInputOnTop: true, + useDynamicMessageLayout: true + }); } } ); @@ -261,7 +271,10 @@ AskQuickQuestionAction.registerMode( QuickQuestionMode.InputOnBottomChat, class InputOnBottomChatQuickQuestionMode extends BaseChatQuickQuestionMode { constructor() { - super(false); + super({ + renderInputOnTop: false, + useDynamicMessageLayout: false + }); } } ); diff --git a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction.ts b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction.ts index 55d1b8faab9f7..a76eb9f6636f1 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction.ts @@ -3,11 +3,13 @@ * 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 { Lazy } from 'vs/base/common/lazy'; import { localize } from 'vs/nls'; -import { Action2 } from 'vs/platform/actions/common/actions'; +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 { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; @@ -25,6 +27,7 @@ export interface IQuickQuestionMode { run(accessor: ServicesAccessor, query: string): void; } +// TODO: This should be registered per chat-provider probably. export class AskQuickQuestionAction extends Action2 { private static readonly modeRegistry: Map> = new Map(); @@ -35,8 +38,9 @@ export class AskQuickQuestionAction extends Action2 { constructor() { super({ id: ASK_QUICK_QUESTION_ACTION_ID, - title: { value: localize('askQuickQuestion', "Ask Quick Question"), original: 'Ask Quick Question' }, + title: { value: localize('chat', "Chat"), original: 'Chat' }, precondition: CONTEXT_PROVIDER_EXISTS, + icon: Codicon.commentDiscussion, f1: false, category: CHAT_CATEGORY, keybinding: { @@ -46,6 +50,12 @@ export class AskQuickQuestionAction extends Action2 { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI } }, + menu: { + id: MenuId.LayoutControlMenu, + group: '0_workbench_toggles', + when: ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'chatView'), + order: 0 + } }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction.ts b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction.ts index b436449a51a0a..606a67c79f0fb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction.ts @@ -61,7 +61,7 @@ class AskSingleQuickQuestionMode implements IQuickQuestionMode { // Setup toggle that will be used to open the chat view const openInChat = new Toggle({ - title: 'Open in chat view', + title: 'Open In Chat View', icon: Codicon.commentDiscussion, isChecked: false, inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 656485ed70a90..b02cb345f131f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -37,7 +37,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import '../common/chatColors'; import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerClearActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { AccessibilityVerbositySettingId, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; 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'; @@ -78,12 +78,24 @@ configurationRegistry.registerConfiguration({ description: nls.localize('interactiveSession.editor.lineHeight', "Controls the line height in pixels in chat codeblocks. Use 0 to compute the line height from the font size."), default: 0 }, + 'chat.experimental.defaultMode': { + type: 'string', + tags: ['experimental'], + enum: ['chatView', 'quickQuestion', 'both'], + enumDescriptions: [ + nls.localize('interactiveSession.defaultMode.chatView', "Use the chat view as the default mode. Displays the chat icon in the Activity Bar."), + nls.localize('interactiveSession.defaultMode.quickQuestion', "Use the quick question as the default mode. Displays the chat icon in the Title Bar."), + nls.localize('interactiveSession.defaultMode.both', "Displays the chat icon in the Activity Bar and the Title Bar which open their respective chat modes.") + ], + description: nls.localize('interactiveSession.defaultMode', "Controls the default mode of the chat experience."), + default: 'chatView' + }, 'chat.experimental.quickQuestion.mode': { type: 'string', tags: ['experimental'], enum: [QuickQuestionMode.SingleQuestion, QuickQuestionMode.InputOnTopChat, QuickQuestionMode.InputOnBottomChat], description: nls.localize('interactiveSession.quickQuestion.mode', "Controls the mode of quick question chat experience."), - default: QuickQuestionMode.SingleQuestion, + default: QuickQuestionMode.InputOnTopChat, } } }); @@ -135,48 +147,58 @@ class ChatAccessibleViewContribution extends Disposable { const accessibleViewService = accessor.get(IAccessibleViewService); const widgetService = accessor.get(IChatWidgetService); const codeEditorService = accessor.get(ICodeEditorService); + return renderAccessibleView(accessibleViewService, widgetService, codeEditorService, true); + function renderAccessibleView(accessibleViewService: IAccessibleViewService, widgetService: IChatWidgetService, codeEditorService: ICodeEditorService, initialRender?: boolean): boolean { + const widget = widgetService.lastFocusedWidget; + if (!widget) { + return false; + } + const chatInputFocused = initialRender && !!codeEditorService.getFocusedCodeEditor(); + if (initialRender && chatInputFocused) { + widget.focusLastMessage(); + } - let widget = widgetService.lastFocusedWidget; - if (!widget) { - return false; - } - - const chatInputFocused = !!(codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); + if (!widget) { + return false; + } - if (chatInputFocused) { - widget.focusLastMessage(); - widget = widgetService.lastFocusedWidget; - } + const verifiedWidget: IChatWidget = widget; + const focusedItem = verifiedWidget.getFocus(); - if (!widget) { - return false; - } + if (!focusedItem) { + return false; + } - const verifiedWidget: IChatWidget = widget; - const focusedItem = verifiedWidget.getFocus(); + widget.focus(focusedItem); - if (!focusedItem) { - return false; - } + const responseContent = isResponseVM(focusedItem) ? focusedItem.response.value : undefined; + if (!responseContent) { + return false; + } - const responseContent = isResponseVM(focusedItem) ? focusedItem.response.value : undefined; - if (!responseContent) { - return false; + accessibleViewService.show({ + verbositySettingKey: AccessibilityVerbositySettingId.Chat, + provideContent(): string { return responseContent; }, + onClose() { + verifiedWidget.reveal(focusedItem); + if (chatInputFocused) { + verifiedWidget.focusInput(); + } else { + verifiedWidget.focus(focusedItem); + } + }, + next() { + verifiedWidget.moveFocus(focusedItem, 'next'); + renderAccessibleView(accessibleViewService, widgetService, codeEditorService); + }, + previous() { + verifiedWidget.moveFocus(focusedItem, 'previous'); + renderAccessibleView(accessibleViewService, widgetService, codeEditorService); + }, + options: { ariaLabel: nls.localize('chatAccessibleView', "Chat Accessible View"), type: AccessibleViewType.View } + }); + return true; } - - accessibleViewService.show({ - verbositySettingKey: 'panelChat', - provideContent(): string { return responseContent; }, - onClose() { - if (chatInputFocused) { - verifiedWidget.focusInput(); - } else { - verifiedWidget.focus(focusedItem); - } - }, - options: { ariaLabel: nls.localize('chatAccessibleView', "Chat Accessible View"), language: 'typescript', type: AccessibleViewType.View } - }); - return true; }, CONTEXT_IN_CHAT_SESSION)); } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 996a999286cce..da8714f61d782 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -28,6 +28,8 @@ export interface IChatWidgetService { revealViewForProvider(providerId: string): Promise; getWidgetByInputUri(uri: URI): IChatWidget | undefined; + + getWidgetBySessionId(sessionId: string): IChatWidget | undefined; } @@ -56,6 +58,7 @@ export interface IChatWidget { reveal(item: ChatTreeItem): void; focus(item: ChatTreeItem): void; + moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void; getFocus(): ChatTreeItem | undefined; acceptInput(query?: string): void; focusLastMessage(): void; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index 87d9abc836c5a..84cadbfa36423 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -19,6 +19,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi private _responsePendingAudioCue: IDisposable | undefined; private _hasReceivedRequest: boolean = false; private _runOnceScheduler: RunOnceScheduler; + private _lastResponse: string | undefined; constructor(@IAudioCueService private readonly _audioCueService: IAudioCueService) { super(); @@ -37,6 +38,10 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi const isPanelChat = typeof response !== 'string'; this._responsePendingAudioCue?.dispose(); this._runOnceScheduler?.cancel(); + if (this._lastResponse === response?.toString()) { + return; + } + this._lastResponse = response?.toString(); this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, true); this._hasReceivedRequest = false; if (!response) { diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index c4cc0badb4d8d..0176f7c10b652 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -125,7 +125,10 @@ export class ChatContributionService implements IChatContributionService { canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ providerId: providerDescriptor.id }]), - when: ContextKeyExpr.deserialize(providerDescriptor.when), + when: ContextKeyExpr.and( + ContextKeyExpr.deserialize(providerDescriptor.when), + ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'quickQuestion') + ) }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index bbac658da6f45..439afe5cbe5e8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -50,6 +50,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { ILogService } from 'vs/platform/log/common/log'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { ChatTreeItem, IChatCodeBlockInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; @@ -305,6 +306,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { } export class ChatAccessibilityProvider implements IListAccessibilityProvider { + + constructor( + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + ) { + + } getWidgetRole(): AriaRole { return 'list'; } @@ -542,15 +553,21 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider token.type === 'code')?.length ?? 0; switch (codeBlockCount) { case 0: - return element.response.value; + label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1}", element.response.value, accessibleViewHint) : localize('noCodeBlocks', "{0}", element.response.value); + break; case 1: - return localize('singleCodeBlock', "1 code block: {0}", element.response.value); + label = accessibleViewHint ? localize('singleCodeBlockHint', "1 code block: {0} {1}", element.response.value, accessibleViewHint) : localize('singleCodeBlock', "1 code block: {0}", element.response.value); + break; default: - return localize('multiCodeBlock', "{0} code blocks: {1}", codeBlockCount, element.response.value); + label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} code blocks: {1}", codeBlockCount, element.response.value, accessibleViewHint) : localize('multiCodeBlock', "{0} code blocks", codeBlockCount, element.response.value); + break; } + return label; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts index a2fa834d90551..51324840b88ff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts @@ -9,12 +9,14 @@ 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 { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export class SlashCommandContentWidget extends Disposable implements IContentWidget { private _domNode = document.createElement('div'); private _lastSlashCommandText: string | undefined; - constructor(private _editor: ICodeEditor) { + constructor(private _editor: ICodeEditor, private _accessibilityService: IAccessibilityService) { super(); this._domNode.toggleAttribute('hidden', true); @@ -65,5 +67,8 @@ export class SlashCommandContentWidget extends Disposable implements IContentWid range: new Range(1, 1, 1, selection.startColumn), text: null }]); + + // Announce the deletion + this._accessibilityService.alert(localize('exited slash command mode', 'Exited {0} mode', this._lastSlashCommandText)); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 5f7a05375d9ca..410a8e46b3079 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -71,8 +71,8 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { private updateModel(model?: IChatModel | undefined): void { this.modelDisposables.clear(); - model = model ?? (this.chatService.transferredSessionId - ? this.chatService.getOrRestoreSession(this.chatService.transferredSessionId) + model = model ?? (this.chatService.transferredSessionData?.sessionId + ? this.chatService.getOrRestoreSession(this.chatService.transferredSessionData.sessionId) : this.chatService.startSession(this.chatViewOptions.providerId, CancellationToken.None)); if (!model) { throw new Error('Could not start chat session'); @@ -102,7 +102,14 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { })); this._widget.render(parent); - const sessionId = this.chatService.transferredSessionId ?? this.viewState.sessionId; + let sessionId: string | undefined; + if (this.chatService.transferredSessionData) { + sessionId = this.chatService.transferredSessionData.sessionId; + this.viewState.inputValue = this.chatService.transferredSessionData.inputValue; + } else { + sessionId = this.viewState.sessionId; + } + const initialModel = sessionId ? this.chatService.getOrRestoreSession(sessionId) : undefined; this.updateModel(initialModel); } catch (e) { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 38b3a62da6e2a..4586375277c44 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -5,6 +5,7 @@ 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 { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -76,7 +77,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private previousTreeScrollHeight: number = 0; - private viewModelDisposables = new DisposableStore(); + private viewModelDisposables = this._register(new DisposableStore()); private _viewModel: ChatViewModel | undefined; private set viewModel(viewModel: ChatViewModel | undefined) { if (this._viewModel === viewModel) { @@ -92,8 +93,11 @@ export class ChatWidget extends Disposable implements IChatWidget { this.slashCommandsPromise = undefined; this.lastSlashCommands = undefined; + this.getSlashCommands().then(() => { - this.onDidChangeItems(); + if (!this._isDisposed) { + this.onDidChangeItems(); + } }); this._onDidChangeViewModel.fire(); @@ -114,7 +118,8 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatService private readonly chatService: IChatService, @IChatWidgetService chatWidgetService: IChatWidgetService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService + @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); @@ -135,6 +140,12 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.inputPart.inputUri; } + private _isDisposed: boolean = false; + public override dispose(): void { + this._isDisposed = true; + super.dispose(); + } + 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)); @@ -167,6 +178,23 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.focus(); } + moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void { + const items = this.viewModel?.getItems(); + if (!items) { + return; + } + const responseItems = items.filter(i => isResponseVM(i)); + const targetIndex = responseItems.indexOf(item); + if (targetIndex === undefined) { + return; + } + const indexToFocus = type === 'next' ? targetIndex + 1 : targetIndex - 1; + if (indexToFocus < 0 || indexToFocus === responseItems.length - 1) { + return; + } + this.focus(responseItems[indexToFocus]); + } + private onDidChangeItems() { if (this.tree && this.visible) { const treeItems = (this.viewModel?.getItems() ?? []) @@ -193,6 +221,10 @@ export class ChatWidget extends Disposable implements IChatWidget { } }); + if (this._dynamicMessageLayoutData) { + this.layoutDynamicChatTreeItemMode(); + } + const lastItem = treeItems[treeItems.length - 1]?.element; if (lastItem && isResponseVM(lastItem) && lastItem.isComplete) { this.renderFollowups(lastItem.replyFollowups); @@ -216,13 +248,13 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderer.setVisible(visible); if (visible) { - setTimeout(() => { + this._register(disposableTimeout(() => { // Progressive rendering paused while hidden, so start it up again. // Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here) if (this.visible) { this.onDidChangeItems(); } - }, 0); + }, 0)); } } @@ -274,7 +306,7 @@ export class ChatWidget extends Disposable implements IChatWidget { horizontalScrolling: false, supportDynamicHeights: true, hideTwistiesOfChildlessElements: true, - accessibilityProvider: new ChatAccessibilityProvider(), + accessibilityProvider: this._instantiationService.createInstance(ChatAccessibilityProvider), keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO setRowLineHeight: false, overrideStyles: { @@ -336,7 +368,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } private createInput(container: HTMLElement, options?: { renderFollowups: boolean }): void { - this.inputPart = this.instantiationService.createInstance(ChatInputPart, { renderFollowups: options?.renderFollowups ?? true }); + this.inputPart = this._register(this.instantiationService.createInstance(ChatInputPart, { renderFollowups: options?.renderFollowups ?? true })); this.inputPart.render(container, '', this); this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire())); @@ -471,6 +503,47 @@ export class ChatWidget extends Disposable implements IChatWidget { this.listContainer.style.height = `${height - inputPartHeight}px`; } + private _dynamicMessageLayoutData?: { numOfMessages: number; maxHeight: number }; + + // An alternative to layout, this allows you to specify the number of ChatTreeItems + // you want to show, and the max height of the container. It will then layout the + // tree to show that many items. + setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) { + this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight }; + this._register(this.renderer.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode())); + } + + layoutDynamicChatTreeItemMode(allowRecurse = true): void { + if (!this.viewModel) { + return; + } + const inputHeight = this.inputPart.layout(this._dynamicMessageLayoutData!.maxHeight, this.container.offsetWidth); + + const totalMessages = this.viewModel.getItems(); + // grab the last N messages + const messages = totalMessages.slice(-this._dynamicMessageLayoutData!.numOfMessages); + + const needsRerender = messages.some(m => m.currentRenderedHeight === undefined); + const listHeight = needsRerender + ? this._dynamicMessageLayoutData!.maxHeight + : messages.reduce((acc, message) => acc + message.currentRenderedHeight!, 0); + + this.layout( + Math.min( + // we add an additional 25px in order to show that there is scrollable content + inputHeight + listHeight + (totalMessages.length > 2 ? 25 : 0), + this._dynamicMessageLayoutData!.maxHeight + ), + this.container.offsetWidth + ); + + if (needsRerender && allowRecurse) { + // TODO: figure out a better place to reveal the last element + revealLastElement(this.tree); + this.layoutDynamicChatTreeItemMode(false); + } + } + saveState(): void { this.inputPart.saveState(); } @@ -501,6 +574,10 @@ export class ChatWidgetService implements IChatWidgetService { return this._widgets.find(w => isEqual(w.inputUri, uri)); } + getWidgetBySessionId(sessionId: string): ChatWidget | undefined { + return this._widgets.find(w => w.viewModel?.sessionId === sessionId); + } + async revealViewForProvider(providerId: string): Promise { const viewId = this.chatContributionService.getViewIdForProvider(providerId); const view = await this.viewsService.openView(viewId); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 80d2c1989f18d..5cf1bfd36e36c 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -24,6 +24,7 @@ import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const decorationDescription = 'chat'; const slashCommandPlaceholderDecorationType = 'chat-session-detail'; @@ -39,6 +40,7 @@ class InputEditorDecorations extends Disposable { @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IThemeService private readonly themeService: IThemeService, @IChatService private readonly chatService: IChatService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { super(); @@ -139,7 +141,7 @@ class InputEditorDecorations extends Disposable { if (command && inputValue.startsWith(`/${command.command} `)) { if (!this._slashCommandContentWidget) { - this._slashCommandContentWidget = new SlashCommandContentWidget(this.widget.inputEditor); + this._slashCommandContentWidget = new SlashCommandContentWidget(this.widget.inputEditor, this.accessibilityService); this._store.add(this._slashCommandContentWidget); } this._slashCommandContentWidget.setCommandText(command.command); @@ -227,12 +229,12 @@ class SlashCommandCompletions extends Disposable { const withSlash = `/${c.command}`; return { label: withSlash, - insertText: `${withSlash} `, + insertText: c.executeImmediately ? '' : `${withSlash} `, detail: c.detail, range: new Range(1, 1, 1, 1), sortText: c.sortText ?? c.command, kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget }] } : undefined, + command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: withSlash }] } : undefined, }; }) }; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 875cc21505594..28e304809a21b 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -81,6 +81,61 @@ export class ChatRequestModel implements IChatRequestModel { } } + +interface ResponsePart { string: IMarkdownString; resolving?: boolean } +class Response { + private _onDidChangeValue = new Emitter(); + public get onDidChangeValue() { + return this._onDidChangeValue.event; + } + + private _responseParts: ResponsePart[]; + private _responseRepr: IMarkdownString; + + get value(): IMarkdownString { + return this._responseRepr; + } + + constructor(value: IMarkdownString) { + this._responseRepr = value; + this._responseParts = [{ string: value }]; + } + + updateContent(responsePart: string | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean): void { + if (typeof responsePart === 'string') { + const responsePartLength = this._responseParts.length - 1; + const lastResponsePart = this._responseParts[responsePartLength]; + + if (lastResponsePart.resolving === true) { + // The last part is resolving, start a new part + this._responseParts.push({ string: new MarkdownString(responsePart) }); + } else { + // Combine this part with the last, non-resolving part + this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart) }; + } + + this._updateRepr(quiet); + } else { + // Add a new resolving part + const responsePosition = this._responseParts.push({ string: new MarkdownString(responsePart.placeholder), resolving: true }) - 1; + this._updateRepr(quiet); + + responsePart.resolvedContent?.then((content) => { + // Replace the resolving part's content with the resolved response + this._responseParts[responsePosition] = { string: new MarkdownString(content), resolving: true }; + this._updateRepr(quiet); + }); + } + } + + private _updateRepr(quiet?: boolean) { + this._responseRepr = new MarkdownString(this._responseParts.map(r => r.string.value).join('\n\n')); + if (!quiet) { + this._onDidChangeValue.fire(); + } + } +} + export class ChatResponseModel extends Disposable implements IChatResponseModel { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; @@ -112,8 +167,9 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._followups; } + private _response: Response; public get response(): IMarkdownString { - return this._response; + return this._response.value; } public get errorDetails(): IChatResponseErrorDetails | undefined { @@ -133,7 +189,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } constructor( - private _response: IMarkdownString, + _response: IMarkdownString, public readonly session: ChatModel, private _isComplete: boolean = false, private _isCanceled = false, @@ -143,14 +199,13 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel private _followups?: IChatFollowup[] ) { super(); + this._response = new Response(_response); + this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); this._id = 'response_' + ChatResponseModel.nextId++; } - updateContent(responsePart: string, quiet?: boolean) { - this._response = new MarkdownString(this.response.value + responsePart); - if (!quiet) { - this._onDidChange.fire(); - } + updateContent(responsePart: string | { placeholder: string; resolvedContent?: Promise }, quiet?: boolean) { + this._response.updateContent(responsePart, quiet); } setProviderResponseId(providerResponseId: string) { @@ -372,7 +427,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(content, obj.responderUsername, obj.responderAvatarIconUri && URI.revive(obj.responderAvatarIconUri)); + this._welcomeMessage = new ChatWelcomeMessageModel(this, content); } return requests.map((raw: ISerializableChatRequestData) => { @@ -453,6 +508,8 @@ export class ChatModel extends Disposable implements IChatModel { if ('content' in progress) { request.response.updateContent(progress.content, quiet); + } else if ('placeholder' in progress) { + request.response.updateContent(progress, quiet); } else { request.setProviderRequestId(progress.requestId); request.response.setProviderResponseId(progress.requestId); @@ -581,7 +638,18 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { return this._id; } - constructor(public readonly content: IChatWelcomeMessageContent[], public readonly username: string, public readonly avatarIconUri?: URI) { + constructor( + private readonly session: ChatModel, + public readonly content: IChatWelcomeMessageContent[], + ) { this._id = 'welcome_' + ChatWelcomeMessageModel.nextId++; } + + public get username(): string { + return this.session.responderUsername; + } + + public get avatarIconUri(): URI | undefined { + return this.session.responderAvatarIconUri; + } } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index a93be123a49f2..511446cce93b4 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -43,7 +43,7 @@ export interface IChatResponse { } export type IChatProgress = - { content: string } | { requestId: string }; + { content: string } | { requestId: string } | { placeholder: string; resolvedContent: Promise }; export interface IPersistedChatState { } export interface IChatProvider { @@ -189,11 +189,16 @@ export interface IChatProviderInfo { displayName: string; } +export interface IChatTransferredSessionData { + sessionId: string; + inputValue: string; +} + export const IChatService = createDecorator('IChatService'); export interface IChatService { _serviceBrand: undefined; - transferredSessionId: string | undefined; + transferredSessionData: IChatTransferredSessionData | undefined; onDidSubmitSlashCommand: Event<{ slashCommand: string; sessionId: string }>; registerProvider(provider: IChatProvider): IDisposable; @@ -201,6 +206,7 @@ export interface IChatService { getProviderInfos(): IChatProviderInfo[]; startSession(providerId: string, token: CancellationToken): ChatModel | undefined; getSession(sessionId: string): IChatModel | undefined; + getSessionId(sessionProviderId: number): string | undefined; getOrRestoreSession(sessionId: string): IChatModel | undefined; loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined; @@ -221,5 +227,5 @@ export interface IChatService { onDidPerformUserAction: Event; notifyUserAction(event: IChatUserActionEvent): void; - transferChatSession(sessionProviderId: number, toWorkspace: URI): void; + transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 68a14aaf8b7db..fad8eff20973b 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -22,7 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProgress, IChatProvider, IChatProviderInfo, IChatReplyFollowup, IChatService, IChatUserActionEvent, ISlashCommand, ISlashCommandProvider, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProgress, IChatProvider, IChatProviderInfo, IChatReplyFollowup, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, ISlashCommandProvider, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -32,6 +32,7 @@ interface IChatTransfer { toWorkspace: UriComponents; timestampInMilliseconds: number; chat: ISerializableChatData; + inputValue: string; } const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60; @@ -127,9 +128,9 @@ export class ChatService extends Disposable implements IChatService { private readonly _persistedSessions: ISerializableChatsData; private readonly _hasProvider: IContextKey; - private _transferred: ISerializableChatData | undefined; - public get transferredSessionId(): string | undefined { - return this._transferred?.sessionId; + private _transferredSessionData: IChatTransferredSessionData | undefined; + public get transferredSessionData(): IChatTransferredSessionData | undefined { + return this._transferredSessionData; } private readonly _onDidPerformUserAction = this._register(new Emitter()); @@ -162,10 +163,12 @@ export class ChatService extends Disposable implements IChatService { this._persistedSessions = {}; } - this._transferred = this.getTransferredSession(); - if (this._transferred) { - this.trace('constructor', `Transferred session ${this._transferred.sessionId}`); - this._persistedSessions[this._transferred.sessionId] = this._transferred; + const transferredData = this.getTransferredSessionData(); + const transferredChat = transferredData?.chat; + if (transferredChat) { + this.trace('constructor', `Transferred session ${transferredChat.sessionId}`); + this._persistedSessions[transferredChat.sessionId] = transferredChat; + this._transferredSessionData = { sessionId: transferredChat.sessionId, inputValue: transferredData.inputValue }; } this._register(storageService.onWillSaveState(() => this.saveState())); @@ -252,7 +255,7 @@ export class ChatService extends Disposable implements IChatService { } } - private getTransferredSession(): ISerializableChatData | undefined { + private getTransferredSessionData(): IChatTransfer | undefined { const data: IChatTransfer[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []); const workspaceUri = this.workspaceContextService.getWorkspace().folders[0]?.uri; if (!workspaceUri) { @@ -266,7 +269,7 @@ export class ChatService extends Disposable implements IChatService { // Keep data that isn't for the current workspace and that hasn't expired yet const filtered = data.filter(item => URI.revive(item.toWorkspace).toString() !== thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS)); this.storageService.store(globalChatKey, JSON.stringify(filtered), StorageScope.PROFILE, StorageTarget.MACHINE); - return transferred?.chat; + return transferred; } getHistory(): IChatDetail[] { @@ -346,7 +349,7 @@ export class ChatService extends Disposable implements IChatService { const welcomeMessage = model.welcomeMessage ? undefined : withNullAsUndefined(await provider.provideWelcomeMessage?.(token)); const welcomeModel = welcomeMessage && new ChatWelcomeMessageModel( - welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item as IChatReplyFollowup[]), session.responderUsername, session.responderAvatarIconUri); + model, welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item as IChatReplyFollowup[])); model.initialize(session, welcomeModel); } @@ -355,6 +358,10 @@ export class ChatService extends Disposable implements IChatService { return this._sessionModels.get(sessionId); } + getSessionId(sessionProviderId: number): string | undefined { + return Iterable.find(this._sessionModels.values(), model => model.session?.id === sessionProviderId)?.sessionId; + } + getOrRestoreSession(sessionId: string): ChatModel | undefined { const model = this._sessionModels.get(sessionId); if (model) { @@ -366,8 +373,8 @@ export class ChatService extends Disposable implements IChatService { return undefined; } - if (sessionId === this.transferredSessionId) { - this._transferred = undefined; + if (sessionId === this.transferredSessionData?.sessionId) { + this._transferredSessionData = undefined; } return this._startSession(sessionData.providerId, sessionData, CancellationToken.None); @@ -424,6 +431,8 @@ export class ChatService extends Disposable implements IChatService { gotProgress = true; if ('content' in progress) { this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`); + } else if ('placeholder' in progress) { + this.trace('sendRequest', `Provider returned placeholder for session ${model.sessionId}, ${progress.placeholder}`); } else { this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`); } @@ -669,17 +678,18 @@ export class ChatService extends Disposable implements IChatService { }); } - transferChatSession(sessionProviderId: number, toWorkspace: URI): void { - const model = Iterable.find(this._sessionModels.values(), model => model.session?.id === sessionProviderId); + transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { + const model = Iterable.find(this._sessionModels.values(), model => model.sessionId === transferredSessionData.sessionId); if (!model) { - throw new Error(`Failed to transfer session. Unknown session provider ID: ${sessionProviderId}`); + throw new Error(`Failed to transfer session. Unknown session ID: ${transferredSessionData.sessionId}`); } const existingRaw: IChatTransfer[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []); existingRaw.push({ chat: model.toJSON(), timestampInMilliseconds: Date.now(), - toWorkspace: toWorkspace + toWorkspace: toWorkspace, + inputValue: transferredSessionData.inputValue, }); this.storageService.store(globalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 5c3998f7fd07d..14199983c071c 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -368,4 +368,5 @@ export interface IChatWelcomeMessageViewModel { readonly username: string; readonly avatarIconUri?: URI; readonly content: IChatWelcomeMessageContent[]; + currentRenderedHeight?: number; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 903406a294778..d10802456cbab 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -22,11 +22,15 @@ class ToggleScreenReaderMode extends Action2 { id: 'editor.action.toggleScreenReaderAccessibilityMode', title: { value: nls.localize('toggleScreenReaderMode', "Toggle Screen Reader Accessibility Mode"), original: 'Toggle Screen Reader Accessibility Mode' }, f1: true, - keybinding: { + keybinding: [{ primary: KeyMod.CtrlCmd | KeyCode.KeyE, weight: KeybindingWeight.WorkbenchContrib + 10, when: accessibilityHelpIsShown - } + }, + { + primary: KeyMod.Alt | KeyCode.F1 | KeyMod.Shift, + weight: KeybindingWeight.WorkbenchContrib + 10, + }] }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 7f017f54f5cbf..f0da57c7cc944 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -7,7 +7,7 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { DiffReviewNext, DiffReviewPrev } from 'vs/editor/browser/widget/diffEditor.contribution'; +import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor.contribution'; import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; import { EmbeddedDiffEditorWidget, EmbeddedDiffEditorWidget2 } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; @@ -17,7 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { AccessibilityHelpAction, AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { localize } from 'vs/nls'; @@ -86,8 +86,8 @@ function createScreenReaderHelp(): IDisposable { const codeEditorService = accessor.get(ICodeEditorService); const keybindingService = accessor.get(IKeybindingService); - const next = keybindingService.lookupKeybinding(DiffReviewNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(DiffReviewPrev.id)?.getAriaLabel(); + const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); + const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget2)) { return; @@ -101,7 +101,7 @@ function createScreenReaderHelp(): IDisposable { const keys = ['audioCues.diffLineDeleted', 'audioCues.diffLineInserted', 'audioCues.diffLineModified']; accessibleViewService.show({ - verbositySettingKey: 'diffEditor', + verbositySettingKey: AccessibilityVerbositySettingId.DiffEditor, provideContent: () => [ localize('msg1', "You are in a diff editor."), localize('msg2', "Press {0} or {1} to view the next or previous diff in the diff review mode that is optimized for screen readers.", next, previous), diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts index 43eea66d38cdd..5cb892f95cf00 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts @@ -77,7 +77,6 @@ export class EditorLineNumberContextMenu extends Disposable implements IEditorCo return; } - const anchor = { x: e.event.posx, y: e.event.posy }; const lineNumber = e.target.position.lineNumber; const contextKeyService = this.contextKeyService.createOverlay([['editorLineNumber', lineNumber]]); @@ -122,7 +121,7 @@ export class EditorLineNumberContextMenu extends Disposable implements IEditorCo } this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => e.event, getActions: () => Separator.join(...allActions.map((a) => a[1])), onHide: () => menu.dispose(), }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 18ced65b47d27..e4d481e7fe12c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -236,7 +236,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } private _beginCompute(position: Position): void { - const grammar = this._textMateService.createGrammar(this._model.getLanguageId()); + const grammar = this._textMateService.createTokenizer(this._model.getLanguageId()); const semanticTokens = this._computeSemanticTokens(position); dom.clearNode(this._domNode); diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 82f7cd850af9c..7c30e5d516fc5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -47,6 +47,7 @@ import { DomEmitter } from 'vs/base/browser/event'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { FileAccess } from 'vs/base/common/network'; import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; class CommentsActionRunner extends ActionRunner { protected override async runAction(action: IAction, context: any[]): Promise { @@ -690,8 +691,10 @@ export class CommentNode extends Disposable { private onContextMenu(e: MouseEvent) { + const event = new StandardMouseEvent(e); + this.contextMenuService.showContextMenu({ - getAnchor: () => e, + getAnchor: () => event, menuId: MenuId.CommentThreadCommentContext, menuActionOptions: { shouldForwardArgs: true }, contextKeyService: this._contextKeyService, diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 302bec8e1b609..f83748a565dad 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -21,6 +21,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; 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'; 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); @@ -116,8 +117,9 @@ export class CommentThreadHeader extends Disposable { if (!actions.length) { return; } + const event = new StandardMouseEvent(e); this._contextMenuService.showContextMenu({ - getAnchor: () => e, + getAnchor: () => event, getActions: () => actions, actionRunner: new ActionRunner(), getActionsContext: () => { diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 7e6fe3265cd26..b596a2c27dea3 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -829,10 +829,8 @@ export class CommentController implements IEditorContribution { if (newCommentInfos.length > 1) { if (e && range) { - const anchor = { x: e.event.posx, y: e.event.posy }; - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => e.event, getActions: () => this.getContextMenuActions(newCommentInfos, range), getActionsContext: () => newCommentInfos.length ? newCommentInfos[0] : undefined, onHide: () => { this._addInProgress = false; } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 3f40d26457497..964a6a0443ba7 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -22,7 +22,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { IOverlayWebview, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; -import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; interface CustomEditorInputInitInfo { @@ -302,14 +302,6 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return (await this.rename(groupId, target))?.editor; } - override isSaving(): boolean { - if (this.isDirty() && !this.hasCapability(EditorInputCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return true; // will be saved soon - } - - return super.isSaving(); - } - public override async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { if (this._modelRef) { return this._modelRef.object.revert(options); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 0fc7574fbeaa2..8c7e0c62b0eb6 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -728,10 +728,9 @@ class InlineBreakpointWidget implements IContentWidget, IDisposable { })); this.toDispose.push(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, e => { const event = new StandardMouseEvent(e); - const anchor = { x: event.posx, y: event.posy }; const actions = this.getContextMenuActions(); this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => event, getActions: () => actions, getActionsContext: () => this.breakpoint, onHide: () => disposeIfDisposable(actions) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index cf4045bf3ac96..78f7de871fa2e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -78,7 +78,6 @@ 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 { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -473,7 +472,6 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi @IInstantiationService private readonly instantiationService: IInstantiationService, @IDialogService private readonly dialogService: IDialogService, @ICommandService private readonly commandService: ICommandService, - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, ) { super(); const hasGalleryContext = CONTEXT_HAS_GALLERY.bindTo(contextKeyService); @@ -517,31 +515,22 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi // Global actions private registerGlobalActions(): void { - const getTitle = (title: string) => !this.userDataProfileService.currentProfile.isDefault && this.userDataProfileService.currentProfile.useDefaultFlags?.extensions - ? `${title} (${localize('default profile', "Default Profile")})` - : title; - const registerOpenExtensionsActionDisposables = this._register(new DisposableStore()); - const registerOpenExtensionsAction = () => { - registerOpenExtensionsActionDisposables.clear(); - registerOpenExtensionsActionDisposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - command: { - id: VIEWLET_ID, - title: getTitle(localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions")) - }, - group: '2_configuration', - order: 3 - })); - registerOpenExtensionsActionDisposables.add(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - command: { - id: VIEWLET_ID, - title: getTitle(localize('showExtensions', "Extensions")) - }, - group: '2_configuration', - order: 3 - })); - }; - registerOpenExtensionsAction(); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => registerOpenExtensionsAction())); + this._register(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + command: { + id: VIEWLET_ID, + title: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions") + }, + group: '2_configuration', + order: 3 + })); + this._register(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + command: { + id: VIEWLET_ID, + title: localize('showExtensions', "Extensions") + }, + group: '2_configuration', + order: 3 + })); this.registerExtensionAction({ id: 'workbench.extensions.action.installExtensions', @@ -1319,7 +1308,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.ExtensionContext, group: INSTALL_ACTIONS_GROUP, order: 2, - when: ContextKeyExpr.and(ContextKeyExpr.not('installedExtensionIsPreReleaseVersion'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension')) + when: ContextKeyExpr.and(ContextKeyExpr.not('installedExtensionIsPreReleaseVersion'), ContextKeyExpr.not('installedExtensionIsOptedTpPreRelease'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension')) }, run: async (accessor: ServicesAccessor, id: string) => { const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8ad613804b58c..fd41ea7368474 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1006,6 +1006,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['extensionStatus', 'installed']); } cksOverlay.push(['installedExtensionIsPreReleaseVersion', !!extension.local?.isPreReleaseVersion]); + cksOverlay.push(['installedExtensionIsOptedTpPreRelease', !!extension.local?.preRelease]); cksOverlay.push(['galleryExtensionIsPreReleaseVersion', !!extension.gallery?.properties.isPreReleaseVersion]); cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]); cksOverlay.push(['extensionHasReleaseVersion', extension.hasReleaseVersion]); @@ -1194,7 +1195,7 @@ export class SwitchToPreReleaseVersionAction extends ExtensionAction { } update(): void { - this.enabled = !!this.extension && !this.extension.isBuiltin && !this.extension.local?.isPreReleaseVersion && this.extension.hasPreReleaseVersion && this.extension.state === ExtensionState.Installed; + this.enabled = !!this.extension && !this.extension.isBuiltin && !this.extension.local?.isPreReleaseVersion && !this.extension.local?.preRelease && this.extension.hasPreReleaseVersion && this.extension.state === ExtensionState.Installed; } override async run(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index de295f7e1565d..96659a5f22864 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1127,7 +1127,7 @@ 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 (extension.version !== runningExtension.version || extension.local.targetPlatform !== runningExtension.targetPlatform) { + 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."); } diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index fed47c1958c02..996682a745537 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -55,6 +55,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private readonly recommendationsByPattern = new Map>(); private readonly fileBasedRecommendations = new Map(); private readonly fileBasedImportantRecommendations = new Set(); + private readonly processedFileExtensions: string[] = []; get recommendations(): ReadonlyArray { const recommendations: ExtensionRecommendation[] = []; @@ -156,7 +157,11 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return; } - this.promptRecommendedExtensionForFileExtension(uri, extname(uri).toLowerCase()); + const fileExtension = extname(uri).toLowerCase(); + if (!this.processedFileExtensions.includes(fileExtension)) { + this.processedFileExtensions.push(fileExtension); + this.promptRecommendedExtensionForFileExtension(uri, fileExtension); + } } /** diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index cc2b21ddd26b9..e7c5a31c6191c 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -100,7 +100,7 @@ export class FeedbackWidget extends Disposable { })); } - private getAnchor(): HTMLElement | IAnchor { + private getAnchor(): IAnchor { const dimension = this.layoutService.dimension; return { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index daef8334fe59b..93742f3e7d8ad 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -7,14 +7,20 @@ import { registerAction2 } from 'vs/platform/actions/common/actions'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { IInlineChatSessionService, InlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook'; +import { AccessibilityVerbositySettingId, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { localize } from 'vs/nls'; +import { Extensions, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed); registerSingleton(IInlineChatSessionService, InlineChatSessionService, InstantiationType.Delayed); @@ -51,3 +57,41 @@ registerAction2(InlineChatActions.CopyRecordings); Registry.as(Extensions.Workbench) .registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); + + +class InlineChatAccessibleViewContribution extends Disposable { + static ID: 'inlineChatAccessibleViewContribution'; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(100, 'inlineChat', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const codeEditorService = accessor.get(ICodeEditorService); + + const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); + if (!editor) { + return false; + } + const controller = InlineChatController.get(editor); + if (!controller) { + return false; + } + const responseContent = controller?.getMessage(); + if (!responseContent) { + return false; + } + accessibleViewService.show({ + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + + options: { ariaLabel: localize('inlineChatAccessibleView', "Inline Chat Accessible View"), type: AccessibleViewType.View } + }); + return true; + }, ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatAccessibleViewContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 95fef85528594..fa9f70e5179c5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -7,10 +7,10 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; -import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget2 } from 'vs/editor/browser/widget/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_WIDGET, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, 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, InlineChatResponseType, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChateResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, MENU_INLINE_CHAT_WIDGET_TOGGLE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, 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, InlineChatResponseType, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChateResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize } from 'vs/nls'; import { IAction2Options, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -121,7 +121,7 @@ abstract class AbstractInlineChatAction extends EditorAction2 { if (!ctrl) { for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { - if (diffEditor instanceof EmbeddedDiffEditorWidget) { + if (diffEditor instanceof EmbeddedDiffEditorWidget2) { this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args); } } @@ -279,7 +279,7 @@ export class PreviousFromHistory extends AbstractInlineChatAction { super({ id: 'inlineChat.previousFromHistory', title: localize('previousFromHistory', 'Previous From History'), - precondition: CTX_INLINE_CHAT_FOCUSED, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_START), keybinding: { weight: KeybindingWeight.EditorCore + 10, // win against core_command primary: KeyMod.CtrlCmd | KeyCode.UpArrow, @@ -298,7 +298,7 @@ export class NextFromHistory extends AbstractInlineChatAction { super({ id: 'inlineChat.nextFromHistory', title: localize('nextFromHistory', 'Next From History'), - precondition: CTX_INLINE_CHAT_FOCUSED, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END), keybinding: { weight: KeybindingWeight.EditorCore + 10, // win against core_command primary: KeyMod.CtrlCmd | KeyCode.DownArrow, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 9128753de797f..b661e993b4804 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -158,6 +158,10 @@ export class InlineChatController implements IEditorContribution { } } + getMessage(): string | undefined { + return this._zone.value.widget.responseContent; + } + getId(): string { return INLINE_CHAT_ID; } @@ -176,13 +180,17 @@ export class InlineChatController implements IEditorContribution { return this._zone.value.position; } + private _currentRun?: Promise; + async run(options: InlineChatRunOptions | undefined = {}): Promise { - this._log('session starting'); - await this.finishExistingSession(); + this.finishExistingSession(); + if (this._currentRun) { + await this._currentRun; + } this._stashedSession.clear(); - - await this._nextState(State.CREATE_SESSION, options); - this._log('session done or paused'); + this._currentRun = this._nextState(State.CREATE_SESSION, options); + await this._currentRun; + this._currentRun = undefined; } // ---- state machine @@ -304,10 +312,16 @@ export class InlineChatController implements IEditorContribution { this._zone.value.widget.preferredExpansionState = this._activeSession.lastExpansionState; this._zone.value.widget.value = this._activeSession.lastInput?.value ?? this._zone.value.widget.value; this._zone.value.widget.onDidChangeInput(_ => { - const pos = this._zone.value.position; - if (pos && this._zone.value.widget.hasFocus() && this._zone.value.widget.value) { - this._editor.revealPosition(pos, ScrollType.Smooth); + const start = this._zone.value.position; + if (!start || !this._zone.value.widget.hasFocus() || !this._zone.value.widget.value || !this._editor.hasModel()) { + return; } + const nextLine = start.lineNumber + 1; + if (nextLine >= this._editor.getModel().getLineCount()) { + // last line isn't supported + return; + } + this._editor.revealLine(nextLine, ScrollType.Smooth); }); this._showWidget(true, options.position); @@ -620,6 +634,7 @@ export class InlineChatController implements IEditorContribution { } } this._ctxResponseTypes.set(responseTypes); + this._ctxDidEdit.set(this._activeSession.hasChangedText); if (response instanceof EmptyResponse) { // show status message @@ -727,6 +742,10 @@ export class InlineChatController implements IEditorContribution { } } + private static isEditOrMarkdownResponse(response: EditResponse | MarkdownResponse | EmptyResponse | ErrorResponse | undefined): response is EditResponse | MarkdownResponse { + return response instanceof EditResponse || response instanceof MarkdownResponse; + } + // ---- controller API acceptInput(): void { @@ -783,7 +802,7 @@ export class InlineChatController implements IEditorContribution { } feedbackLast(helpful: boolean) { - if (this._activeSession?.lastExchange?.response instanceof EditResponse || this._activeSession?.lastExchange?.response instanceof MarkdownResponse) { + if (this._activeSession?.lastExchange && InlineChatController.isEditOrMarkdownResponse(this._activeSession.lastExchange.response)) { const kind = helpful ? InlineChatResponseFeedbackKind.Helpful : InlineChatResponseFeedbackKind.Unhelpful; this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, kind); this._ctxLastFeedbackKind.set(helpful ? 'helpful' : 'unhelpful'); @@ -798,24 +817,22 @@ export class InlineChatController implements IEditorContribution { } acceptSession(): void { + if (this._activeSession?.lastExchange && InlineChatController.isEditOrMarkdownResponse(this._activeSession.lastExchange.response)) { + this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, InlineChatResponseFeedbackKind.Accepted); + } this._messages.fire(Message.ACCEPT_SESSION); } cancelSession() { - let result: string | undefined; - if (this._strategy && this._activeSession) { - const changedText = this._activeSession.asChangedText(); - if (changedText && this._activeSession?.lastExchange?.response instanceof EditResponse) { - this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, InlineChatResponseFeedbackKind.Undone); - - } - result = changedText; + const result = this._activeSession?.asChangedText(); + if (this._activeSession?.lastExchange && InlineChatController.isEditOrMarkdownResponse(this._activeSession.lastExchange.response)) { + this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, InlineChatResponseFeedbackKind.Undone); } this._messages.fire(Message.CANCEL_SESSION); return result; } - async finishExistingSession(): Promise { + finishExistingSession(): void { if (this._activeSession) { if (this._activeSession.editMode === EditMode.Preview) { this._log('finishing existing session, using CANCEL', this._activeSession.editMode); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts index 3f38c66ae4ea3..1374da2009011 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts @@ -34,6 +34,7 @@ import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessi import { ILanguageService } from 'vs/editor/common/languages/language'; import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; import { WordHighlighterContribution } from 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export class InlineChatLivePreviewWidget extends ZoneWidget { @@ -53,6 +54,7 @@ export class InlineChatLivePreviewWidget extends ZoneWidget { @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, ordinal: 10000 + 1 }); super.create(); @@ -78,7 +80,8 @@ export class InlineChatLivePreviewWidget extends ZoneWidget { stickyScroll: { enabled: false }, minimap: { enabled: false }, isInEmbeddedEditor: true, - overflowWidgetsDomNode: editor.getOverflowWidgetsDomNode() + overflowWidgetsDomNode: editor.getOverflowWidgetsDomNode(), + onlyShowAccessibleDiffViewer: this.accessibilityService.isScreenReaderOptimized(), }, { originalEditor: { contributions: diffContributions }, modifiedEditor: { contributions: diffContributions } @@ -249,10 +252,13 @@ export class InlineChatLivePreviewWidget extends ZoneWidget { return; } - let hiddenRanges = lineRanges.map(lineRangeAsRange); - if (LineRange.fromRange(hiddenRanges.reduce(Range.plusRange)).equals(LineRange.ofLength(1, editor.getModel().getLineCount()))) { - // TODO not every line can be hidden, keep the first line around + 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(lineRangeAsRange); } editor.setHiddenAreas(hiddenRanges, InlineChatLivePreviewWidget._hideId); this._logService.debug(`[IE] diff HIDING ${hiddenRanges} for ${editor.getId()} with ${String(editor.getModel()?.uri)}`); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index c46e3387aa0d8..c3a08e55ce986 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -66,7 +66,9 @@ export class PreviewStrategy extends EditModeStrategy { this._ctxDocumentChanged = CTX_INLINE_CHAT_DOCUMENT_CHANGED.bindTo(contextKeyService); this._listener = Event.debounce(_session.textModelN.onDidChangeContent.bind(_session.textModelN), () => { }, 350)(_ => { - this._ctxDocumentChanged.set(!_session.textModelN.equalsTextBuffer(_session.textModel0.getTextBuffer())); + if (!_session.textModelN.isDisposed() && !_session.textModel0.isDisposed()) { + this._ctxDocumentChanged.set(_session.hasChangedText); + } }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 4cde6edcb51c8..5ee3feeae517d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -12,7 +12,7 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { CTX_INLINE_CHAT_FOCUSED, 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, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, MENU_INLINE_CHAT_WIDGET_TOGGLE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { EventType, Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom'; import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event'; @@ -51,6 +51,8 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { IMenuWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -160,7 +162,10 @@ export class InlineChatWidget { private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>; private readonly _ctxInnerCursorFirst: IContextKey; private readonly _ctxInnerCursorLast: IContextKey; + private readonly _ctxInnerCursorStart: IContextKey; + private readonly _ctxInnerCursorEnd: IContextKey; private readonly _ctxInputEditorFocused: IContextKey; + private readonly _ctxResponseFocused: IContextKey; private readonly _progressBar: ProgressBar; @@ -196,6 +201,7 @@ export class InlineChatWidget { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService ) { // input editor logic @@ -213,6 +219,9 @@ export class InlineChatWidget { 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.message, 'focus', () => this._ctxResponseFocused.set(true))); + this._store.add(addDisposableListener(this._elements.message, 'blur', () => this._ctxResponseFocused.reset())); + this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { this._updateAriaLabel(); @@ -230,13 +239,33 @@ export class InlineChatWidget { this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); this._ctxInnerCursorLast = CTX_INLINE_CHAT_INNER_CURSOR_LAST.bindTo(this._contextKeyService); + this._ctxInnerCursorStart = CTX_INLINE_CHAT_INNER_CURSOR_START.bindTo(this._contextKeyService); + this._ctxInnerCursorEnd = CTX_INLINE_CHAT_INNER_CURSOR_END.bindTo(this._contextKeyService); this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this._contextKeyService); + this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); // (1) inner cursor position (last/first line selected) const updateInnerCursorFirstLast = () => { - const { lineNumber } = this._inputEditor.getPosition(); - this._ctxInnerCursorFirst.set(lineNumber === 1); - this._ctxInnerCursorLast.set(lineNumber === this._inputModel.getLineCount()); + const selection = this._inputEditor.getSelection(); + const fullRange = this._inputModel.getFullModelRange(); + let onFirst = false; + let onLast = false; + if (selection.isEmpty()) { + const selectionTop = this._inputEditor.getTopForPosition(selection.startLineNumber, selection.startColumn); + const firstViewLineTop = this._inputEditor.getTopForPosition(fullRange.startLineNumber, fullRange.startColumn); + const lastViewLineTop = this._inputEditor.getTopForPosition(fullRange.endLineNumber, fullRange.endColumn); + + if (selectionTop === firstViewLineTop) { + onFirst = true; + } + if (selectionTop === lastViewLineTop) { + onLast = true; + } + } + this._ctxInnerCursorFirst.set(onFirst); + this._ctxInnerCursorLast.set(onLast); + this._ctxInnerCursorStart.set(fullRange.getStartPosition().equals(selection.getStartPosition())); + this._ctxInnerCursorEnd.set(fullRange.getEndPosition().equals(selection.getEndPosition())); }; this._store.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast)); updateInnerCursorFirstLast(); @@ -286,7 +315,7 @@ export class InlineChatWidget { // slash command content widget - this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor); + this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor, this._accessibilityService); this._store.add(this._slashCommandContentWidget); // toolbars @@ -329,12 +358,16 @@ export class InlineChatWidget { this._store.add(feedbackToolbar); // preview editors - this._previewDiffEditor = new IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget2, this._elements.previewDiff, _previewEditorEditorOptions, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor))); + this._previewDiffEditor = new IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedDiffEditorWidget2, this._elements.previewDiff, { + ..._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 IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor))); this._elements.message.tabIndex = 0; + this._elements.message.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); 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())); @@ -345,7 +378,8 @@ export class InlineChatWidget { })); } - private _onContextMenu(event: MouseEvent) { + private _onContextMenu(e: MouseEvent) { + const event = new StandardMouseEvent(e); this._contextMenuService.showContextMenu({ menuId: MENU_INLINE_CHAT_WIDGET_TOGGLE, getAnchor: () => event, @@ -465,6 +499,10 @@ export class InlineChatWidget { this._preferredExpansionState = expansionState; } + get responseContent(): string | undefined { + return this._elements.markdownMessage.textContent ?? undefined; + } + updateMarkdownMessage(message: Node | undefined) { this._elements.markdownMessage.classList.toggle('hidden', !message); let expansionState: ExpansionState; @@ -761,7 +799,7 @@ export class InlineChatZoneWidget extends ZoneWidget { if (!this.widget.hasFocus()) { this.widget.focus(); } - })); + }, true)); // todo@jrieken listen ONLY when showing const updateCursorIsAboveContextKey = () => { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 855ffa543011a..0249658958bc0 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -91,7 +91,8 @@ export interface IInlineChatProgressItem { export const enum InlineChatResponseFeedbackKind { Unhelpful = 0, Helpful = 1, - Undone = 2 + Undone = 2, + Accepted = 3 } export interface IInlineChatSessionProvider { @@ -120,9 +121,12 @@ export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccess export const CTX_INLINE_CHAT_HAS_PROVIDER = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); +export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); 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")); 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 f4ba0f5b182ac..49db6a4ff686e 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -27,6 +27,8 @@ import { equals } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; suite('InteractiveChatController', function () { @@ -104,6 +106,11 @@ suite('InteractiveChatController', function () { [IChatAccessibilityService, new class extends mock() { override acceptResponse(response?: IChatResponseViewModel): void { } override acceptRequest(): void { } + }], + [IAccessibleViewService, new class extends mock() { + override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { + return null; + } }] ); @@ -148,9 +155,9 @@ suite('InteractiveChatController', function () { test('run (show/hide)', async function () { ctrl = instaService.createInstance(TestController, editor); + const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); const run = ctrl.run({ message: 'Hello', autoSend: true }); - - await ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); + await p; assert.ok(ctrl.getWidgetPosition() !== undefined); ctrl.cancelSession(); @@ -218,9 +225,10 @@ suite('InteractiveChatController', function () { test('typing outside of wholeRange finishes session', async function () { ctrl = instaService.createInstance(TestController, editor); + const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); ctrl.run({ message: 'Hello', autoSend: true }); - await ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); + await p; const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); assert.ok(session); @@ -257,9 +265,10 @@ suite('InteractiveChatController', function () { }); store.add(d); ctrl = instaService.createInstance(TestController, editor); + const p = ctrl.waitFor(TestController.INIT_SEQUENCE); ctrl.run({ message: 'Hello', autoSend: false }); - await ctrl.waitFor(TestController.INIT_SEQUENCE); + await p; const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); assert.ok(session); @@ -298,12 +307,13 @@ suite('InteractiveChatController', function () { }); store.add(d); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.run({ message: 'Hello', autoSend: true }); + const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.MAKE_REQUEST]); + const r = ctrl.run({ message: 'Hello', autoSend: true }); - await ctrl.waitFor([...TestController.INIT_SEQUENCE, State.MAKE_REQUEST]); + await p; ctrl.acceptSession(); - await p; + await r; assert.strictEqual(ctrl.getWidgetPosition(), undefined); }); }); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index c97adbabfe2bd..30fff709556ce 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import 'vs/css!./media/interactive'; import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; @@ -83,40 +81,39 @@ export interface InteractiveEditorOptions extends ITextEditorOptions { } export class InteractiveEditor extends EditorPane { - #rootElement!: HTMLElement; - #styleElement!: HTMLStyleElement; - #notebookEditorContainer!: HTMLElement; - #notebookWidget: IBorrowValue = { value: undefined }; - #inputCellContainer!: HTMLElement; - #inputFocusIndicator!: HTMLElement; - #inputRunButtonContainer!: HTMLElement; - #inputEditorContainer!: HTMLElement; - #codeEditorWidget!: CodeEditorWidget; - // #inputLineCount = 1; - #notebookWidgetService: INotebookEditorService; - #instantiationService: IInstantiationService; - #languageService: ILanguageService; - #contextKeyService: IContextKeyService; - #configurationService: IConfigurationService; - #notebookKernelService: INotebookKernelService; - #keybindingService: IKeybindingService; - #menuService: IMenuService; - #contextMenuService: IContextMenuService; - #editorGroupService: IEditorGroupsService; - #notebookExecutionStateService: INotebookExecutionStateService; - #extensionService: IExtensionService; - #widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); - #lastLayoutDimensions?: { readonly dimension: DOM.Dimension; readonly position: DOM.IDomPosition }; - #editorOptions: IEditorOptions; - #notebookOptions: NotebookOptions; - #editorMemento: IEditorMemento; - #groupListener = this._register(new DisposableStore()); - #runbuttonToolbar: ToolBar | undefined; - - #onDidFocusWidget = this._register(new Emitter()); - override get onDidFocus(): Event { return this.#onDidFocusWidget.event; } - #onDidChangeSelection = this._register(new Emitter()); - readonly onDidChangeSelection = this.#onDidChangeSelection.event; + private _rootElement!: HTMLElement; + private _styleElement!: HTMLStyleElement; + private _notebookEditorContainer!: HTMLElement; + private _notebookWidget: IBorrowValue = { value: undefined }; + private _inputCellContainer!: HTMLElement; + private _inputFocusIndicator!: HTMLElement; + private _inputRunButtonContainer!: HTMLElement; + private _inputEditorContainer!: HTMLElement; + private _codeEditorWidget!: CodeEditorWidget; + private _notebookWidgetService: INotebookEditorService; + private _instantiationService: IInstantiationService; + private _languageService: ILanguageService; + private _contextKeyService: IContextKeyService; + private _configurationService: IConfigurationService; + private _notebookKernelService: INotebookKernelService; + private _keybindingService: IKeybindingService; + private _menuService: IMenuService; + private _contextMenuService: IContextMenuService; + private _editorGroupService: IEditorGroupsService; + private _notebookExecutionStateService: INotebookExecutionStateService; + private _extensionService: IExtensionService; + private _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); + private _lastLayoutDimensions?: { readonly dimension: DOM.Dimension; readonly position: DOM.IDomPosition }; + private _editorOptions: IEditorOptions; + private _notebookOptions: NotebookOptions; + private _editorMemento: IEditorMemento; + private _groupListener = this._register(new DisposableStore()); + 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; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -143,68 +140,68 @@ export class InteractiveEditor extends EditorPane { themeService, storageService ); - this.#instantiationService = instantiationService; - this.#notebookWidgetService = notebookWidgetService; - this.#contextKeyService = contextKeyService; - this.#configurationService = configurationService; - this.#notebookKernelService = notebookKernelService; - this.#languageService = languageService; - this.#keybindingService = keybindingService; - this.#menuService = menuService; - this.#contextMenuService = contextMenuService; - this.#editorGroupService = editorGroupService; - this.#notebookExecutionStateService = notebookExecutionStateService; - this.#extensionService = extensionService; - - this.#editorOptions = this.#computeEditorOptions(); - this._register(this.#configurationService.onDidChangeConfiguration(e => { + this._instantiationService = instantiationService; + this._notebookWidgetService = notebookWidgetService; + this._contextKeyService = contextKeyService; + this._configurationService = configurationService; + this._notebookKernelService = notebookKernelService; + this._languageService = languageService; + this._keybindingService = keybindingService; + this._menuService = menuService; + this._contextMenuService = contextMenuService; + this._editorGroupService = editorGroupService; + this._notebookExecutionStateService = notebookExecutionStateService; + this._extensionService = extensionService; + + this._editorOptions = this._computeEditorOptions(); + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { - this.#editorOptions = this.#computeEditorOptions(); + this._editorOptions = this._computeEditorOptions(); } })); - this.#notebookOptions = new NotebookOptions(configurationService, notebookExecutionStateService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, dragAndDropEnabled: false }); - this.#editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); + this._notebookOptions = new NotebookOptions(configurationService, notebookExecutionStateService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScroll: false, dragAndDropEnabled: false }); + this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); - this._register(this.#keybindingService.onDidUpdateKeybindings(this.#updateInputDecoration, this)); - this._register(this.#notebookExecutionStateService.onDidChangeExecution((e) => { - if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this.#notebookWidget.value?.viewModel?.notebookDocument.uri)) { - const cell = this.#notebookWidget.value?.getCellByHandle(e.cellHandle); + this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputDecoration, this)); + this._register(this._notebookExecutionStateService.onDidChangeExecution((e) => { + if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this._notebookWidget.value?.viewModel?.notebookDocument.uri)) { + const cell = this._notebookWidget.value?.getCellByHandle(e.cellHandle); if (cell && e.changed?.state) { - this.#scrollIfNecessary(cell); + this._scrollIfNecessary(cell); } } })); } - get #inputCellContainerHeight() { + private get inputCellContainerHeight() { return 19 + 2 + INPUT_CELL_VERTICAL_PADDING * 2 + INPUT_EDITOR_PADDING * 2; } - get #inputCellEditorHeight() { + private get inputCellEditorHeight() { return 19 + INPUT_EDITOR_PADDING * 2; } protected createEditor(parent: HTMLElement): void { - this.#rootElement = DOM.append(parent, DOM.$('.interactive-editor')); - this.#rootElement.style.position = 'relative'; - this.#notebookEditorContainer = DOM.append(this.#rootElement, DOM.$('.notebook-editor-container')); - this.#inputCellContainer = DOM.append(this.#rootElement, DOM.$('.input-cell-container')); - this.#inputCellContainer.style.position = 'absolute'; - this.#inputCellContainer.style.height = `${this.#inputCellContainerHeight}px`; - this.#inputFocusIndicator = DOM.append(this.#inputCellContainer, DOM.$('.input-focus-indicator')); - this.#inputRunButtonContainer = DOM.append(this.#inputCellContainer, DOM.$('.run-button-container')); - this.#setupRunButtonToolbar(this.#inputRunButtonContainer); - this.#inputEditorContainer = DOM.append(this.#inputCellContainer, DOM.$('.input-editor-container')); - this.#createLayoutStyles(); + this._rootElement = DOM.append(parent, DOM.$('.interactive-editor')); + this._rootElement.style.position = 'relative'; + this._notebookEditorContainer = DOM.append(this._rootElement, DOM.$('.notebook-editor-container')); + this._inputCellContainer = DOM.append(this._rootElement, DOM.$('.input-cell-container')); + this._inputCellContainer.style.position = 'absolute'; + this._inputCellContainer.style.height = `${this.inputCellContainerHeight}px`; + this._inputFocusIndicator = DOM.append(this._inputCellContainer, DOM.$('.input-focus-indicator')); + this._inputRunButtonContainer = DOM.append(this._inputCellContainer, DOM.$('.run-button-container')); + this._setupRunButtonToolbar(this._inputRunButtonContainer); + this._inputEditorContainer = DOM.append(this._inputCellContainer, DOM.$('.input-editor-container')); + this._createLayoutStyles(); } - #setupRunButtonToolbar(runButtonContainer: HTMLElement) { - const menu = this._register(this.#menuService.createMenu(MenuId.InteractiveInputExecute, this.#contextKeyService)); - this.#runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this.#contextMenuService, { - getKeyBinding: action => this.#keybindingService.lookupKeybinding(action.id), + private _setupRunButtonToolbar(runButtonContainer: HTMLElement) { + const menu = this._register(this._menuService.createMenu(MenuId.InteractiveInputExecute, this._contextKeyService)); + this._runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this._contextMenuService, { + getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id), actionViewItemProvider: action => { - return createActionViewItem(this.#instantiationService, action); + return createActionViewItem(this._instantiationService, action); }, renderDropdownAsChildElement: true })); @@ -214,18 +211,18 @@ export class InteractiveEditor extends EditorPane { const result = { primary, secondary }; createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); - this.#runbuttonToolbar.setActions([...primary, ...secondary]); + this._runbuttonToolbar.setActions([...primary, ...secondary]); } - #createLayoutStyles(): void { - this.#styleElement = DOM.createStyleSheet(this.#rootElement); + private _createLayoutStyles(): void { + this._styleElement = DOM.createStyleSheet(this._rootElement); const styleSheets: string[] = []; const { focusIndicator, codeCellLeftMargin, cellRunGutter - } = this.#notebookOptions.getLayoutConfiguration(); + } = this._notebookOptions.getLayoutConfiguration(); const leftMargin = codeCellLeftMargin + cellRunGutter; styleSheets.push(` @@ -269,16 +266,16 @@ export class InteractiveEditor extends EditorPane { } `); - this.#styleElement.textContent = styleSheets.join('\n'); + this._styleElement.textContent = styleSheets.join('\n'); } - #computeEditorOptions(): IEditorOptions { + private _computeEditorOptions(): IEditorOptions { let overrideIdentifier: string | undefined = undefined; - if (this.#codeEditorWidget) { - overrideIdentifier = this.#codeEditorWidget.getModel()?.getLanguageId(); + if (this._codeEditorWidget) { + overrideIdentifier = this._codeEditorWidget.getModel()?.getLanguageId(); } - const editorOptions = deepClone(this.#configurationService.getValue('editor', { overrideIdentifier })); - const editorOptionsOverride = getSimpleEditorOptions(this.#configurationService); + const editorOptions = deepClone(this._configurationService.getValue('editor', { overrideIdentifier })); + const editorOptionsOverride = getSimpleEditorOptions(this._configurationService); const computed = Object.freeze({ ...editorOptions, ...editorOptionsOverride, @@ -298,7 +295,7 @@ export class InteractiveEditor extends EditorPane { } protected override saveState(): void { - this.#saveEditorViewState(this.input); + this._saveEditorViewState(this.input); super.saveState(); } @@ -308,39 +305,39 @@ export class InteractiveEditor extends EditorPane { return undefined; } - this.#saveEditorViewState(input); - return this.#loadNotebookEditorViewState(input); + this._saveEditorViewState(input); + return this._loadNotebookEditorViewState(input); } - #saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this.#notebookWidget.value && input instanceof InteractiveEditorInput) { - if (this.#notebookWidget.value.isDisposed) { + private _saveEditorViewState(input: EditorInput | undefined): void { + if (this.group && this._notebookWidget.value && input instanceof InteractiveEditorInput) { + if (this._notebookWidget.value.isDisposed) { return; } - const state = this.#notebookWidget.value.getEditorViewState(); - const editorState = this.#codeEditorWidget.saveViewState(); - this.#editorMemento.saveEditorState(this.group, input.notebookEditorInput.resource, { + const state = this._notebookWidget.value.getEditorViewState(); + const editorState = this._codeEditorWidget.saveViewState(); + this._editorMemento.saveEditorState(this.group, input.notebookEditorInput.resource, { notebook: state, input: editorState }); } } - #loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { + private _loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { let result: InteractiveEditorViewState | undefined; if (this.group) { - result = this.#editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); + result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); } if (result) { return result; } // when we don't have a view state for the group/input-tuple then we try to use an existing // editor for the same resource. - for (const group of this.#editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + for (const group of this._editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { if (group.activeEditorPane !== this && group.activeEditorPane === this && group.activeEditor?.matches(input)) { - const notebook = this.#notebookWidget.value?.getEditorViewState(); - const input = this.#codeEditorWidget.saveViewState(); + const notebook = this._notebookWidget.value?.getEditorViewState(); + const input = this._codeEditorWidget.saveViewState(); return { notebook, input @@ -356,13 +353,13 @@ export class InteractiveEditor extends EditorPane { // there currently is a widget which we still own so // we need to hide it before getting a new widget - this.#notebookWidget.value?.onWillHide(); + this._notebookWidget.value?.onWillHide(); - this.#codeEditorWidget?.dispose(); + this._codeEditorWidget?.dispose(); - this.#widgetDisposableStore.clear(); + this._widgetDisposableStore.clear(); - this.#notebookWidget = >this.#instantiationService.invokeFunction(this.#notebookWidgetService.retrieveWidget, group, notebookInput, { + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, notebookInput, { isEmbedded: true, isReadOnly: true, contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ @@ -385,10 +382,10 @@ export class InteractiveEditor extends EditorPane { ModesHoverController.ID, MarkerController.ID ]), - options: this.#notebookOptions + options: this._notebookOptions }); - this.#codeEditorWidget = this.#instantiationService.createInstance(CodeEditorWidget, this.#inputEditorContainer, this.#editorOptions, { + this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { ...{ isSimpleWidget: false, contributions: EditorExtensionsRegistry.getSomeEditorContributions([ @@ -405,109 +402,109 @@ export class InteractiveEditor extends EditorPane { } }); - if (this.#lastLayoutDimensions) { - this.#notebookEditorContainer.style.height = `${this.#lastLayoutDimensions.dimension.height - this.#inputCellContainerHeight}px`; - this.#notebookWidget.value!.layout(new DOM.Dimension(this.#lastLayoutDimensions.dimension.width, this.#lastLayoutDimensions.dimension.height - this.#inputCellContainerHeight), this.#notebookEditorContainer); + if (this._lastLayoutDimensions) { + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._notebookWidget.value!.layout(new DOM.Dimension(this._lastLayoutDimensions.dimension.width, this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight), this._notebookEditorContainer); const { codeCellLeftMargin, cellRunGutter - } = this.#notebookOptions.getLayoutConfiguration(); + } = this._notebookOptions.getLayoutConfiguration(); const leftMargin = codeCellLeftMargin + cellRunGutter; - const maxHeight = Math.min(this.#lastLayoutDimensions.dimension.height / 2, this.#inputCellEditorHeight); - this.#codeEditorWidget.layout(this.#validateDimension(this.#lastLayoutDimensions.dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); - this.#inputFocusIndicator.style.height = `${this.#inputCellEditorHeight}px`; - this.#inputCellContainer.style.top = `${this.#lastLayoutDimensions.dimension.height - this.#inputCellContainerHeight}px`; - this.#inputCellContainer.style.width = `${this.#lastLayoutDimensions.dimension.width}px`; + const maxHeight = Math.min(this._lastLayoutDimensions.dimension.height / 2, this.inputCellEditorHeight); + this._codeEditorWidget.layout(this._validateDimension(this._lastLayoutDimensions.dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${this.inputCellEditorHeight}px`; + this._inputCellContainer.style.top = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${this._lastLayoutDimensions.dimension.width}px`; } await super.setInput(input, options, context, token); const model = await input.resolve(); - if (this.#runbuttonToolbar) { - this.#runbuttonToolbar.context = input.resource; + if (this._runbuttonToolbar) { + this._runbuttonToolbar.context = input.resource; } if (model === null) { throw new Error('The Interactive Window model could not be resolved'); } - this.#notebookWidget.value?.setParentContextKeyService(this.#contextKeyService); + this._notebookWidget.value?.setParentContextKeyService(this._contextKeyService); - const viewState = options?.viewState ?? this.#loadNotebookEditorViewState(input); - await this.#extensionService.whenInstalledExtensionsRegistered(); - await this.#notebookWidget.value!.setModel(model.notebook, viewState?.notebook); - model.notebook.setCellCollapseDefault(this.#notebookOptions.getCellCollapseDefault()); - this.#notebookWidget.value!.setOptions({ + const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input); + await this._extensionService.whenInstalledExtensionsRegistered(); + await this._notebookWidget.value!.setModel(model.notebook, viewState?.notebook); + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); + this._notebookWidget.value!.setOptions({ isReadOnly: true }); - this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidResizeOutput((cvm) => { - this.#scrollIfNecessary(cvm); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidResizeOutput((cvm) => { + this._scrollIfNecessary(cvm); })); - this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidFocusWidget(() => this.#onDidFocusWidget.fire())); - this.#widgetDisposableStore.add(this.#notebookOptions.onDidChangeOptions(e => { + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidFocusWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._notebookOptions.onDidChangeOptions(e => { if (e.compactView || e.focusIndicator) { // update the styling - this.#styleElement?.remove(); - this.#createLayoutStyles(); + this._styleElement?.remove(); + this._createLayoutStyles(); } - if (this.#lastLayoutDimensions && this.isVisible()) { - this.layout(this.#lastLayoutDimensions.dimension, this.#lastLayoutDimensions.position); + if (this._lastLayoutDimensions && this.isVisible()) { + this.layout(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); } if (e.interactiveWindowCollapseCodeCells) { - model.notebook.setCellCollapseDefault(this.#notebookOptions.getCellCollapseDefault()); + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); } })); - const languageId = this.#notebookWidget.value?.activeKernel?.supportedLanguages[0] ?? input.language ?? PLAINTEXT_LANGUAGE_ID; + const languageId = this._notebookWidget.value?.activeKernel?.supportedLanguages[0] ?? input.language ?? PLAINTEXT_LANGUAGE_ID; const editorModel = await input.resolveInput(languageId); editorModel.setLanguage(languageId); - this.#codeEditorWidget.setModel(editorModel); + this._codeEditorWidget.setModel(editorModel); if (viewState?.input) { - this.#codeEditorWidget.restoreViewState(viewState.input); + this._codeEditorWidget.restoreViewState(viewState.input); } - this.#editorOptions = this.#computeEditorOptions(); - this.#codeEditorWidget.updateOptions(this.#editorOptions); + this._editorOptions = this._computeEditorOptions(); + this._codeEditorWidget.updateOptions(this._editorOptions); - this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidFocusEditorWidget(() => this.#onDidFocusWidget.fire())); - this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidContentSizeChange(e => { + this._widgetDisposableStore.add(this._codeEditorWidget.onDidFocusEditorWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidContentSizeChange(e => { if (!e.contentHeightChanged) { return; } - if (this.#lastLayoutDimensions) { - this.#layoutWidgets(this.#lastLayoutDimensions.dimension, this.#lastLayoutDimensions.position); + if (this._lastLayoutDimensions) { + this._layoutWidgets(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); } })); - this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeCursorPosition(e => this.#onDidChangeSelection.fire({ reason: this.#toEditorPaneSelectionChangeReason(e) }))); - this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeModelContent(() => this.#onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(e => this._onDidChangeSelection.fire({ reason: this._toEditorPaneSelectionChangeReason(e) }))); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); - this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeNotebookAffinity(this.#syncWithKernel, this)); - this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeSelectedNotebooks(this.#syncWithKernel, this)); + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeNotebookAffinity(this._syncWithKernel, this)); + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeSelectedNotebooks(this._syncWithKernel, this)); - this.#widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => { + this._widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => { if (this.isVisible()) { - this.#updateInputDecoration(); + this._updateInputDecoration(); } })); - this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeModelContent(() => { + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => { if (this.isVisible()) { - this.#updateInputDecoration(); + this._updateInputDecoration(); } })); - const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this.#contextKeyService); + const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this._contextKeyService); if (input.resource && input.historyService.has(input.resource)) { cursorAtBoundaryContext.set('top'); } else { cursorAtBoundaryContext.set('none'); } - this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeCursorPosition(({ position }) => { - const viewModel = this.#codeEditorWidget._getViewModel()!; + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(({ position }) => { + const viewModel = this._codeEditorWidget._getViewModel()!; const lastLineNumber = viewModel.getLineCount(); const lastLineCol = viewModel.getLineContent(lastLineNumber).length + 1; const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); @@ -529,22 +526,22 @@ export class InteractiveEditor extends EditorPane { } })); - this.#widgetDisposableStore.add(editorModel.onDidChangeContent(() => { + this._widgetDisposableStore.add(editorModel.onDidChangeContent(() => { const value = editorModel!.getValue(); if (this.input?.resource && value !== '') { (this.input as InteractiveEditorInput).historyService.replaceLast(this.input.resource, value); } })); - this.#syncWithKernel(); + this._syncWithKernel(); } override setOptions(options: INotebookEditorOptions | undefined): void { - this.#notebookWidget.value?.setOptions(options); + this._notebookWidget.value?.setOptions(options); super.setOptions(options); } - #toEditorPaneSelectionChangeReason(e: ICursorPositionChangedEvent): EditorPaneSelectionChangeReason { + private _toEditorPaneSelectionChangeReason(e: ICursorPositionChangedEvent): EditorPaneSelectionChangeReason { switch (e.source) { case TextEditorSelectionSource.PROGRAMMATIC: return EditorPaneSelectionChangeReason.PROGRAMMATIC; case TextEditorSelectionSource.NAVIGATION: return EditorPaneSelectionChangeReason.NAVIGATION; @@ -553,31 +550,31 @@ export class InteractiveEditor extends EditorPane { } } - #cellAtBottom(cell: ICellViewModel): boolean { - const visibleRanges = this.#notebookWidget.value?.visibleRanges || []; - const cellIndex = this.#notebookWidget.value?.getCellIndex(cell); + private _cellAtBottom(cell: ICellViewModel): boolean { + const visibleRanges = this._notebookWidget.value?.visibleRanges || []; + const cellIndex = this._notebookWidget.value?.getCellIndex(cell); if (cellIndex === Math.max(...visibleRanges.map(range => range.end - 1))) { return true; } return false; } - #scrollIfNecessary(cvm: ICellViewModel) { - const index = this.#notebookWidget.value!.getCellIndex(cvm); - if (index === this.#notebookWidget.value!.getLength() - 1) { + private _scrollIfNecessary(cvm: ICellViewModel) { + const index = this._notebookWidget.value!.getCellIndex(cvm); + if (index === this._notebookWidget.value!.getLength() - 1) { // If we're already at the bottom or auto scroll is enabled, scroll to the bottom - if (this.#configurationService.getValue(InteractiveWindowSetting.interactiveWindowAlwaysScrollOnNewCell) || this.#cellAtBottom(cvm)) { - this.#notebookWidget.value!.scrollToBottom(); + if (this._configurationService.getValue(InteractiveWindowSetting.interactiveWindowAlwaysScrollOnNewCell) || this._cellAtBottom(cvm)) { + this._notebookWidget.value!.scrollToBottom(); } } } - #syncWithKernel() { - const notebook = this.#notebookWidget.value?.textModel; - const textModel = this.#codeEditorWidget.getModel(); + private _syncWithKernel() { + const notebook = this._notebookWidget.value?.textModel; + const textModel = this._codeEditorWidget.getModel(); if (notebook && textModel) { - const info = this.#notebookKernelService.getMatchingKernel(notebook); + const info = this._notebookKernelService.getMatchingKernel(notebook); const selectedOrSuggested = info.selected ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) ?? (info.all.length === 1 ? info.all[0] : undefined); @@ -586,75 +583,75 @@ export class InteractiveEditor extends EditorPane { const language = selectedOrSuggested.supportedLanguages[0]; // All kernels will initially list plaintext as the supported language before they properly initialized. if (language && language !== 'plaintext') { - const newMode = this.#languageService.createById(language).languageId; + const newMode = this._languageService.createById(language).languageId; textModel.setLanguage(newMode); } - NOTEBOOK_KERNEL.bindTo(this.#contextKeyService).set(selectedOrSuggested.id); + NOTEBOOK_KERNEL.bindTo(this._contextKeyService).set(selectedOrSuggested.id); } } - this.#updateInputDecoration(); + this._updateInputDecoration(); } layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void { - this.#rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); - this.#rootElement.classList.toggle('narrow-width', dimension.width < 600); - const editorHeightChanged = dimension.height !== this.#lastLayoutDimensions?.dimension.height; - this.#lastLayoutDimensions = { dimension, position }; + this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); + this._rootElement.classList.toggle('narrow-width', dimension.width < 600); + const editorHeightChanged = dimension.height !== this._lastLayoutDimensions?.dimension.height; + this._lastLayoutDimensions = { dimension, position }; - if (!this.#notebookWidget.value) { + if (!this._notebookWidget.value) { return; } - if (editorHeightChanged && this.#codeEditorWidget) { - SuggestController.get(this.#codeEditorWidget)?.cancelSuggestWidget(); + if (editorHeightChanged && this._codeEditorWidget) { + SuggestController.get(this._codeEditorWidget)?.cancelSuggestWidget(); } - this.#notebookEditorContainer.style.height = `${this.#lastLayoutDimensions.dimension.height - this.#inputCellContainerHeight}px`; - this.#layoutWidgets(dimension, position); + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._layoutWidgets(dimension, position); } - #layoutWidgets(dimension: DOM.Dimension, position: DOM.IDomPosition) { - const contentHeight = this.#codeEditorWidget.hasModel() ? this.#codeEditorWidget.getContentHeight() : this.#inputCellEditorHeight; + private _layoutWidgets(dimension: DOM.Dimension, position: DOM.IDomPosition) { + const contentHeight = this._codeEditorWidget.hasModel() ? this._codeEditorWidget.getContentHeight() : this.inputCellEditorHeight; const maxHeight = Math.min(dimension.height / 2, contentHeight); const { codeCellLeftMargin, cellRunGutter - } = this.#notebookOptions.getLayoutConfiguration(); + } = this._notebookOptions.getLayoutConfiguration(); const leftMargin = codeCellLeftMargin + cellRunGutter; const inputCellContainerHeight = maxHeight + INPUT_CELL_VERTICAL_PADDING * 2; - this.#notebookEditorContainer.style.height = `${dimension.height - inputCellContainerHeight}px`; + this._notebookEditorContainer.style.height = `${dimension.height - inputCellContainerHeight}px`; - this.#notebookWidget.value!.layout(dimension.with(dimension.width, dimension.height - inputCellContainerHeight), this.#notebookEditorContainer, position); - this.#codeEditorWidget.layout(this.#validateDimension(dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); - this.#inputFocusIndicator.style.height = `${contentHeight}px`; - this.#inputCellContainer.style.top = `${dimension.height - inputCellContainerHeight}px`; - this.#inputCellContainer.style.width = `${dimension.width}px`; + this._notebookWidget.value!.layout(dimension.with(dimension.width, dimension.height - inputCellContainerHeight), this._notebookEditorContainer, position); + this._codeEditorWidget.layout(this._validateDimension(dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${contentHeight}px`; + this._inputCellContainer.style.top = `${dimension.height - inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${dimension.width}px`; } - #validateDimension(width: number, height: number) { + private _validateDimension(width: number, height: number) { return new DOM.Dimension(Math.max(0, width), Math.max(0, height)); } - #updateInputDecoration(): void { - if (!this.#codeEditorWidget) { + private _updateInputDecoration(): void { + if (!this._codeEditorWidget) { return; } - if (!this.#codeEditorWidget.hasModel()) { + if (!this._codeEditorWidget.hasModel()) { return; } - const model = this.#codeEditorWidget.getModel(); + const model = this._codeEditorWidget.getModel(); const decorations: IDecorationOptions[] = []; if (model?.getValueLength() === 0) { const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); const languageId = model.getLanguageId(); - const keybinding = this.#keybindingService.lookupKeybinding('interactive.execute', this.#contextKeyService)?.getLabel(); + const keybinding = this._keybindingService.lookupKeybinding('interactive.execute', this._contextKeyService)?.getLabel(); const text = nls.localize('interactiveInputPlaceHolder', "Type '{0}' code here and press {1} to run", languageId, keybinding ?? 'ctrl+enter'); decorations.push({ range: { @@ -672,51 +669,51 @@ export class InteractiveEditor extends EditorPane { }); } - this.#codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); + this._codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); } override focus() { - this.#notebookWidget.value?.onShow(); - this.#codeEditorWidget.focus(); + this._notebookWidget.value?.onShow(); + this._codeEditorWidget.focus(); } focusHistory() { - this.#notebookWidget.value!.focus(); + 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))); + this._groupListener.clear(); + this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); } if (!visible) { - this.#saveEditorViewState(this.input); - if (this.input && this.#notebookWidget.value) { - this.#notebookWidget.value.onWillHide(); + this._saveEditorViewState(this.input); + if (this.input && this._notebookWidget.value) { + this._notebookWidget.value.onWillHide(); } } } override clearInput() { - if (this.#notebookWidget.value) { - this.#saveEditorViewState(this.input); - this.#notebookWidget.value.onWillHide(); + if (this._notebookWidget.value) { + this._saveEditorViewState(this.input); + this._notebookWidget.value.onWillHide(); } - this.#codeEditorWidget?.dispose(); + this._codeEditorWidget?.dispose(); - this.#notebookWidget = { value: undefined }; - this.#widgetDisposableStore.clear(); + this._notebookWidget = { value: undefined }; + this._widgetDisposableStore.clear(); super.clearInput(); } override getControl(): { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } { return { - notebookEditor: this.#notebookWidget.value, - codeEditor: this.#codeEditorWidget + notebookEditor: this._notebookWidget.value, + codeEditor: this._codeEditorWidget }; } } diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveHistoryService.ts b/src/vs/workbench/contrib/interactive/browser/interactiveHistoryService.ts index f39479fa10ffc..cdfbf09592b69 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveHistoryService.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveHistoryService.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { HistoryNavigator2 } from 'vs/base/common/history'; import { Disposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; @@ -26,21 +24,21 @@ export interface IInteractiveHistoryService { export class InteractiveHistoryService extends Disposable implements IInteractiveHistoryService { declare readonly _serviceBrand: undefined; - #history: ResourceMap>; + _history: ResourceMap>; constructor() { super(); - this.#history = new ResourceMap>(); + this._history = new ResourceMap>(); } addToHistory(uri: URI, value: string): void { - if (!this.#history.has(uri)) { - this.#history.set(uri, new HistoryNavigator2([value], 50)); + if (!this._history.has(uri)) { + this._history.set(uri, new HistoryNavigator2([value], 50)); return; } - const history = this.#history.get(uri)!; + const history = this._history.get(uri)!; history.resetCursor(); if (history?.current() !== value) { @@ -48,22 +46,22 @@ export class InteractiveHistoryService extends Disposable implements IInteractiv } } getPreviousValue(uri: URI): string | null { - const history = this.#history.get(uri); + const history = this._history.get(uri); return history?.previous() ?? null; } getNextValue(uri: URI): string | null { - const history = this.#history.get(uri); + const history = this._history.get(uri); return history?.next() ?? null; } replaceLast(uri: URI, value: string) { - if (!this.#history.has(uri)) { - this.#history.set(uri, new HistoryNavigator2([value], 50)); + if (!this._history.has(uri)) { + this._history.set(uri, new HistoryNavigator2([value], 50)); return; } else { - const history = this.#history.get(uri); + const history = this._history.get(uri); if (history?.current() !== value) { history?.replaceLast(value); } @@ -72,11 +70,11 @@ export class InteractiveHistoryService extends Disposable implements IInteractiv } clearHistory(uri: URI) { - this.#history.delete(uri); + this._history.delete(uri); } has(uri: URI) { - return this.#history.has(uri) ? true : false; + return this._history.has(uri) ? true : false; } } diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 622adff44b562..0cf38581b03be 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -132,14 +132,12 @@ pre code { } .vscode-light h1, -.vscode-light h2, .vscode-light hr, .vscode-light td { border-color: rgba(0, 0, 0, 0.18); } .vscode-dark h1, -.vscode-dark h2, .vscode-dark hr, .vscode-dark td { border-color: rgba(255, 255, 255, 0.18); 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 41d051edbb838..ed90ad3289482 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts @@ -617,6 +617,7 @@ registerAction2(class ToggleCellOutputScrolling extends NotebookMultiCellAction const currentlyEnabled = cellMetadata['scrollable'] !== undefined ? cellMetadata['scrollable'] : globalScrollSetting; const shouldEnableScrolling = collapsed || !currentlyEnabled; cellMetadata['scrollable'] = shouldEnableScrolling; + viewModel.model.bumpVersion(); viewModel.resetRenderer(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts index 33ec159850729..de1b477cf67b3 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts @@ -47,7 +47,7 @@ export class ContributedStatusBarItemController extends Disposable implements IN added: ICellViewModel[]; removed: { handle: number }[]; }): void { - const vm = this._notebookEditor._getViewModel(); + const vm = this._notebookEditor.getViewModel(); if (!vm) { return; } 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 3aa72fc3bfb97..34a10466dd1cb 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -58,7 +58,7 @@ export class NotebookStatusBarController extends Disposable { } private _updateVisibleCells(e: ICellVisibilityChangeEvent): void { - const vm = this._notebookEditor._getViewModel(); + const vm = this._notebookEditor.getViewModel(); if (!vm) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/execute/executionEditorProgress.ts b/src/vs/workbench/contrib/notebook/browser/contrib/execute/executionEditorProgress.ts index 312f780362447..ccda24744d23e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/execute/executionEditorProgress.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/execute/executionEditorProgress.ts @@ -42,8 +42,6 @@ export class ExecutionEditorProgressController extends Disposable implements INo return; } - const scrollPadding = this._notebookEditor.notebookOptions.computeTopInsertToolbarHeight(this._notebookEditor.textModel.viewType); - const cellExecutions = this._notebookExecutionStateService.getCellExecutionsForNotebook(this._notebookEditor.textModel?.uri) .filter(exe => exe.state === NotebookCellExecutionState.Executing); const notebookExecution = this._notebookExecutionStateService.getExecution(this._notebookEditor.textModel?.uri); @@ -52,7 +50,7 @@ export class ExecutionEditorProgressController extends Disposable implements INo for (const cell of this._notebookEditor.getCellsInRange(range)) { if (cell.handle === exe.cellHandle) { const top = this._notebookEditor.getAbsoluteTopOfElement(cell); - if (this._notebookEditor.scrollTop < top + scrollPadding + 5) { + if (this._notebookEditor.scrollTop < top + 5) { return true; } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index 5c58ffda1bc1f..da74cbb1a7646 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -125,7 +125,7 @@ export class FindModel extends Disposable { // we only update cell state if users are using the hybrid mode (both input and preview are enabled) const updateEditingState = () => { - const viewModel = this._notebookEditor._getViewModel() as NotebookViewModel | undefined; + const viewModel = this._notebookEditor.getViewModel() as NotebookViewModel | undefined; if (!viewModel) { return; } @@ -164,7 +164,7 @@ export class FindModel extends Disposable { if (e.isReplaceRevealed && !this._state.isReplaceRevealed) { // replace is hidden, we need to switch all markdown cells to preview mode - const viewModel = this._notebookEditor._getViewModel() as NotebookViewModel | undefined; + const viewModel = this._notebookEditor.getViewModel() as NotebookViewModel | undefined; if (!viewModel) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index a5536043a40f2..e0c09afffbcfd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -187,7 +187,7 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi const replacePattern = this.replacePattern; const replaceString = replacePattern.buildReplaceString(match.matches, this._state.preserveCase); - const viewModel = this._notebookEditor._getViewModel(); + const viewModel = this._notebookEditor.getViewModel(); viewModel.replaceOne(cell, match.range, replaceString).then(() => { this._progressBar.stop(); }); @@ -215,7 +215,7 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi }); }); - const viewModel = this._notebookEditor._getViewModel(); + const viewModel = this._notebookEditor.getViewModel(); viewModel.replaceAll(this._findModel.findMatches, replaceStrings).then(() => { this._progressBar.stop(); }); 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 facce42cfcf31..6b28f787ee43c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -431,7 +431,7 @@ registerAction2(class extends NotebookCellAction { function getPageSize(context: INotebookCellActionContext) { const editor = context.notebookEditor; - const layoutInfo = editor._getViewModel().layoutInfo; + const layoutInfo = editor.getViewModel().layoutInfo; const lineHeight = layoutInfo?.fontInfo.lineHeight || 17; return Math.max(1, Math.floor((layoutInfo?.height || 0) / lineHeight) - 2); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index acd6f718da6d2..ea0f4250cf290 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -3,138 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./notebookOutline'; -import { Codicon } from 'vs/base/common/codicons'; -import { Emitter, Event } from 'vs/base/common/event'; -import { combinedDisposable, IDisposable, Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { CellRevealType, IActiveNotebookEditor, ICellViewModel, INotebookEditorOptions, INotebookEditorPane, INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigCollapseItemsValues, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IEditorPane } from 'vs/workbench/common/editor'; +import { localize } from 'vs/nls'; +import { IIconLabelValueOptions, IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; -import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { IdleValue } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { getIconClassesForLanguageId } from 'vs/editor/common/services/getIconClasses'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { getIconClassesForLanguageId } from 'vs/editor/common/services/getIconClasses'; import { IWorkbenchDataTreeOptions } from 'vs/platform/list/browser/listService'; -import { localize } from 'vs/nls'; -import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { Registry } from 'vs/platform/registry/common/platform'; import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; -import { isEqual } from 'vs/base/common/resources'; -import { IdleValue } from 'vs/base/common/async'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; -import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { executingStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { URI } from 'vs/base/common/uri'; -import { getMarkdownHeadersInCell } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; - -export interface IOutlineMarkerInfo { - readonly count: number; - readonly topSev: MarkerSeverity; -} - -export class OutlineEntry { - - private _children: OutlineEntry[] = []; - private _parent: OutlineEntry | undefined; - private _markerInfo: IOutlineMarkerInfo | undefined; - - get icon(): ThemeIcon { - return this.isExecuting && this.isPaused ? executingStateIcon : - this.isExecuting ? ThemeIcon.modify(executingStateIcon, 'spin') : - this.cell.cellKind === CellKind.Markup ? Codicon.markdown : Codicon.code; - } - - constructor( - readonly index: number, - readonly level: number, - readonly cell: ICellViewModel, - readonly label: string, - readonly isExecuting: boolean, - readonly isPaused: boolean - ) { } - - addChild(entry: OutlineEntry) { - this._children.push(entry); - entry._parent = this; - } - - get parent(): OutlineEntry | undefined { - return this._parent; - } - - get children(): Iterable { - return this._children; - } - - get markerInfo(): IOutlineMarkerInfo | undefined { - return this._markerInfo; - } - - updateMarkers(markerService: IMarkerService): void { - if (this.cell.cellKind === CellKind.Code) { - // a code cell can have marker - const marker = markerService.read({ resource: this.cell.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); - if (marker.length === 0) { - this._markerInfo = undefined; - } else { - const topSev = marker.find(a => a.severity === MarkerSeverity.Error)?.severity ?? MarkerSeverity.Warning; - this._markerInfo = { topSev, count: marker.length }; - } - } else { - // a markdown cell can inherit markers from its children - let topChild: MarkerSeverity | undefined; - for (const child of this.children) { - child.updateMarkers(markerService); - if (child.markerInfo) { - topChild = !topChild ? child.markerInfo.topSev : Math.max(child.markerInfo.topSev, topChild); - } - } - this._markerInfo = topChild && { topSev: topChild, count: 0 }; - } - } - - clearMarkers(): void { - this._markerInfo = undefined; - for (const child of this.children) { - child.clearMarkers(); - } - } - - find(cell: ICellViewModel, parents: OutlineEntry[]): OutlineEntry | undefined { - if (cell.id === this.cell.id) { - return this; - } - parents.push(this); - for (const child of this.children) { - const result = child.find(cell, parents); - if (result) { - return result; - } - } - parents.pop(); - return undefined; - } +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { CellRevealType, INotebookEditorOptions, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { NotebookCellOutlineProvider, OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigCollapseItemsValues, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; - asFlatList(bucket: OutlineEntry[]): void { - bucket.push(this); - for (const child of this.children) { - child.asFlatList(bucket); - } - } -} class NotebookOutlineTemplate { @@ -294,73 +193,51 @@ export class NotebookCellOutline implements IOutline { readonly onDidChange: Event = this._onDidChange.event; - private _uri: URI | undefined; - private _entries: OutlineEntry[] = []; - private _activeEntry?: OutlineEntry; + get entries(): OutlineEntry[] { + return this._outlineProvider?.entries ?? []; + } + private readonly _entriesDisposables = new DisposableStore(); readonly config: IOutlineListConfig; + readonly outlineKind = 'notebookCells'; get activeElement(): OutlineEntry | undefined { - return this._activeEntry; + return this._outlineProvider?.activeElement; } + private _outlineProvider: NotebookCellOutlineProvider | undefined; + constructor( private readonly _editor: INotebookEditorPane, - private readonly _target: OutlineTarget, + _target: OutlineTarget, @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, @IEditorService private readonly _editorService: IEditorService, - @IMarkerService private readonly _markerService: IMarkerService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, + @IConfigurationService _configurationService: IConfigurationService, ) { - const selectionListener = new MutableDisposable(); - this._dispoables.add(selectionListener); const installSelectionListener = () => { const notebookEditor = _editor.getControl(); if (!notebookEditor?.hasModel()) { - selectionListener.clear(); + this._outlineProvider?.dispose(); + this._outlineProvider = undefined; } else { - selectionListener.value = combinedDisposable( - Event.debounce( - notebookEditor.onDidChangeSelection, - (last, _current) => last, - 200 - )(this._recomputeActive, this), - Event.debounce( - notebookEditor.onDidChangeViewCells, - (last, _current) => last ?? _current, - 200 - )(this._recomputeState, this) - ); + this._outlineProvider?.dispose(); + this._outlineProvider = instantiationService.createInstance(NotebookCellOutlineProvider, notebookEditor, _target); } }; this._dispoables.add(_editor.onDidChangeModel(() => { - this._recomputeState(); installSelectionListener(); })); - this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.outline.showCodeCells')) { - this._recomputeState(); - } - })); - - this._dispoables.add(themeService.onDidFileIconThemeChange(() => { - this._onDidChange.fire({}); - })); - this._dispoables.add(_notebookExecutionStateService.onDidChangeExecution(e => { - if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) { - this._recomputeState(); - } - })); - this._recomputeState(); installSelectionListener(); + const treeDataSource: IDataSource = { getChildren: parent => parent instanceof NotebookCellOutline ? (this._outlineProvider?.entries ?? []) : parent.children }; + const delegate = new NotebookOutlineVirtualDelegate(); + const renderers = [instantiationService.createInstance(NotebookOutlineRenderer)]; + const comparator = new NotebookComparator(); const options: IWorkbenchDataTreeOptions = { collapseByDefault: _target === OutlineTarget.Breadcrumbs || (_target === OutlineTarget.OutlinePane && _configurationService.getValue(OutlineConfigKeys.collapseItems) === OutlineConfigCollapseItemsValues.Collapsed), @@ -371,16 +248,11 @@ export class NotebookCellOutline implements IOutline { keyboardNavigationLabelProvider: new NotebookNavigationLabelProvider() }; - const treeDataSource: IDataSource = { getChildren: parent => parent instanceof NotebookCellOutline ? this._entries : parent.children }; - const delegate = new NotebookOutlineVirtualDelegate(); - const renderers = [instantiationService.createInstance(NotebookOutlineRenderer)]; - const comparator = new NotebookComparator(); - this.config = { breadcrumbsDataSource: { getBreadcrumbElements: () => { const result: OutlineEntry[] = []; - let candidate = this._activeEntry; + let candidate = this.activeElement; while (candidate) { result.unshift(candidate); candidate = candidate.parent; @@ -388,7 +260,7 @@ export class NotebookCellOutline implements IOutline { return result; } }, - quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => this._entries), + quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => (this._outlineProvider?.entries ?? [])), treeDataSource, delegate, renderers, @@ -397,221 +269,12 @@ export class NotebookCellOutline implements IOutline { }; } - dispose(): void { - this._onDidChange.dispose(); - this._dispoables.dispose(); - this._entriesDisposables.dispose(); + get uri(): URI | undefined { + return this._outlineProvider?.uri; } - - private _recomputeState(): void { - this._entriesDisposables.clear(); - this._activeEntry = undefined; - this._entries.length = 0; - this._uri = undefined; - - const notebookEditorControl = this._editor.getControl(); - - if (!notebookEditorControl) { - return; - } - - if (!notebookEditorControl.hasModel()) { - return; - } - - this._uri = notebookEditorControl.textModel.uri; - - const notebookEditorWidget: IActiveNotebookEditor = notebookEditorControl; - - if (notebookEditorWidget.getLength() === 0) { - return; - } - - let includeCodeCells = true; - if (this._target === OutlineTarget.OutlinePane) { - includeCodeCells = this._configurationService.getValue('notebook.outline.showCodeCells'); - } else if (this._target === OutlineTarget.Breadcrumbs) { - includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); - } - - const focusedCellIndex = notebookEditorWidget.getFocus().start; - const focused = notebookEditorWidget.cellAt(focusedCellIndex)?.handle; - const entries: OutlineEntry[] = []; - - for (let i = 0; i < notebookEditorWidget.getLength(); i++) { - const cell = notebookEditorWidget.cellAt(i); - const isMarkdown = cell.cellKind === CellKind.Markup; - if (!isMarkdown && !includeCodeCells) { - continue; - } - - // cap the amount of characters that we look at and use the following logic - // - for MD prefer headings (each header is an entry) - // - otherwise use the first none-empty line of the cell (MD or code) - let content = this._getCellFirstNonEmptyLine(cell); - let hasHeader = false; - - if (isMarkdown) { - const fullContent = cell.getText().substring(0, 10_000); - for (const { depth, text } of getMarkdownHeadersInCell(fullContent)) { - hasHeader = true; - entries.push(new OutlineEntry(entries.length, depth, cell, text, false, false)); - } - - if (!hasHeader) { - // no markdown syntax headers, try to find html tags - const match = fullContent.match(/(.*)<\/h\1>/i); - if (match) { - hasHeader = true; - const level = parseInt(match[1]); - const text = match[2].trim(); - entries.push(new OutlineEntry(entries.length, level, cell, text, false, false)); - } - } - - if (!hasHeader) { - content = renderMarkdownAsPlaintext({ value: content }); - } - } - - if (!hasHeader) { - let preview = content.trim(); - if (preview.length === 0) { - // empty or just whitespace - preview = localize('empty', "empty cell"); - } - - const exeState = !isMarkdown && this._notebookExecutionStateService.getCellExecution(cell.uri); - entries.push(new OutlineEntry(entries.length, 7, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); - } - - if (cell.handle === focused) { - this._activeEntry = entries[entries.length - 1]; - } - - // send an event whenever any of the cells change - this._entriesDisposables.add(cell.model.onDidChangeContent(() => { - this._recomputeState(); - this._onDidChange.fire({}); - })); - } - - // build a tree from the list of entries - if (entries.length > 0) { - const result: OutlineEntry[] = [entries[0]]; - const parentStack: OutlineEntry[] = [entries[0]]; - - for (let i = 1; i < entries.length; i++) { - const entry = entries[i]; - - while (true) { - const len = parentStack.length; - if (len === 0) { - // root node - result.push(entry); - parentStack.push(entry); - break; - - } else { - const parentCandidate = parentStack[len - 1]; - if (parentCandidate.level < entry.level) { - parentCandidate.addChild(entry); - parentStack.push(entry); - break; - } else { - parentStack.pop(); - } - } - } - } - this._entries = result; - } - - // feature: show markers with each cell - const markerServiceListener = new MutableDisposable(); - this._entriesDisposables.add(markerServiceListener); - const updateMarkerUpdater = () => { - if (notebookEditorWidget.isDisposed) { - return; - } - - const doUpdateMarker = (clear: boolean) => { - for (const entry of this._entries) { - if (clear) { - entry.clearMarkers(); - } else { - entry.updateMarkers(this._markerService); - } - } - }; - if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { - markerServiceListener.value = this._markerService.onMarkerChanged(e => { - if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) { - doUpdateMarker(false); - this._onDidChange.fire({}); - } - }); - doUpdateMarker(false); - } else { - markerServiceListener.clear(); - doUpdateMarker(true); - } - }; - updateMarkerUpdater(); - this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { - updateMarkerUpdater(); - this._onDidChange.fire({}); - } - })); - - this._onDidChange.fire({}); - } - - private _recomputeActive(): void { - let newActive: OutlineEntry | undefined; - const notebookEditorWidget = this._editor.getControl(); - - if (notebookEditorWidget) { - if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) { - const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start); - if (cell) { - for (const entry of this._entries) { - newActive = entry.find(cell, []); - if (newActive) { - break; - } - } - } - } - } - if (newActive !== this._activeEntry) { - this._activeEntry = newActive; - this._onDidChange.fire({ affectOnlyActiveElement: true }); - } - } - - private _getCellFirstNonEmptyLine(cell: ICellViewModel) { - const textBuffer = cell.textBuffer; - for (let i = 0; i < textBuffer.getLineCount(); i++) { - const firstNonWhitespace = textBuffer.getLineFirstNonWhitespaceColumn(i + 1); - const lineLength = textBuffer.getLineLength(i + 1); - if (firstNonWhitespace < lineLength) { - return textBuffer.getLineContent(i + 1); - } - } - - return cell.getText().substring(0, 10_000); - } - get isEmpty(): boolean { - return this._entries.length === 0; - } - - get uri() { - return this._uri; + return this._outlineProvider?.isEmpty ?? true; } - async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise { await this._editorService.openEditor({ resource: entry.cell.uri, @@ -646,9 +309,15 @@ export class NotebookCellOutline implements IOutline { } }); } + + dispose(): void { + this._onDidChange.dispose(); + this._dispoables.dispose(); + this._entriesDisposables.dispose(); + } } -class NotebookOutlineCreator implements IOutlineCreator { +export class NotebookOutlineCreator implements IOutlineCreator { readonly dispose: () => void; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index ce2b9849c2ae7..6404536b1df22 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -82,7 +82,7 @@ export class TroubleshootController extends Disposable implements INotebookEdito }); })); - const vm = this._notebookEditor._getViewModel(); + const vm = this._notebookEditor.getViewModel(); let items: INotebookDeltaCellStatusBarItems[] = []; if (this._enabled) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts b/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts index 22678c6af01e7..4a74f442f947a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo.ts @@ -21,7 +21,7 @@ class NotebookUndoRedoContribution extends Disposable { const PRIORITY = 105; this._register(UndoCommand.addImplementation(PRIORITY, 'notebook-undo-redo', () => { const editor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); - const viewModel = editor?._getViewModel() as NotebookViewModel | undefined; + const viewModel = editor?.getViewModel() as NotebookViewModel | undefined; if (editor && editor.hasModel() && viewModel) { return viewModel.undo().then(cellResources => { if (cellResources?.length) { @@ -42,7 +42,7 @@ class NotebookUndoRedoContribution extends Disposable { this._register(RedoCommand.addImplementation(PRIORITY, 'notebook-undo-redo', () => { const editor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); - const viewModel = editor?._getViewModel() as NotebookViewModel | undefined; + const viewModel = editor?.getViewModel() as NotebookViewModel | undefined; if (editor && editor.hasModel() && viewModel) { return viewModel.redo().then(cellResources => { diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts index bc8ea045516d8..651e9a2983744 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts @@ -498,7 +498,7 @@ export async function joinNotebookCells(editor: IActiveNotebookEditor, range: IC export async function joinCellsWithSurrounds(bulkEditService: IBulkEditService, context: INotebookCellActionContext, direction: 'above' | 'below'): Promise { const editor = context.notebookEditor; const textModel = editor.textModel; - const viewModel = editor._getViewModel() as NotebookViewModel; + const viewModel = editor.getViewModel() as NotebookViewModel; let ret: { edits: ResourceEdit[]; cell: ICellViewModel; @@ -656,7 +656,7 @@ export function insertCell( initialText: string = '', ui: boolean = false ) { - const viewModel = editor._getViewModel() as NotebookViewModel; + const viewModel = editor.getViewModel() as NotebookViewModel; const activeKernel = editor.activeKernel; if (viewModel.options.isReadOnly) { return null; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/foldingController.ts b/src/vs/workbench/contrib/notebook/browser/controller/foldingController.ts index 0af3a8546ce90..349d4e2ea9e34 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/foldingController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/foldingController.ts @@ -49,7 +49,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont this._foldingModel = new FoldingModel(); this._localStore.add(this._foldingModel); - this._foldingModel.attachViewModel(this._notebookEditor._getViewModel()); + this._foldingModel.attachViewModel(this._notebookEditor.getViewModel()); this._localStore.add(this._foldingModel.onDidFoldingRegionChanged(() => { this._updateEditorFoldingRanges(); @@ -103,7 +103,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont return; } - const vm = this._notebookEditor._getViewModel() as NotebookViewModel; + const vm = this._notebookEditor.getViewModel() as NotebookViewModel; vm.updateFoldingRanges(this._foldingModel.regions); const hiddenRanges = vm.getHiddenRanges(); @@ -119,7 +119,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont return; } - const viewModel = this._notebookEditor._getViewModel() as NotebookViewModel; + const viewModel = this._notebookEditor.getViewModel() as NotebookViewModel; const target = e.event.target as HTMLElement; if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) { @@ -243,7 +243,7 @@ registerAction2(class extends Action2 { controller.setFoldingStateDown(index, CellFoldingState.Collapsed, levels); } - const viewIndex = editor._getViewModel().getNearestVisibleCellIndexUpwards(index); + const viewIndex = editor.getViewModel().getNearestVisibleCellIndexUpwards(index); editor.focusElement(editor.cellAt(viewIndex)); } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index afcefd23bb6e9..60f3e15045044 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -41,6 +41,7 @@ import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { fixedDiffEditorOptions, fixedEditorOptions, fixedEditorPadding } from 'vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { return { @@ -86,6 +87,7 @@ class PropertyHeader extends Disposable { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService private readonly themeService: IThemeService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(); } @@ -117,7 +119,7 @@ class PropertyHeader extends Disposable { this._toolbar = new WorkbenchToolBar(cellToolbarContainer, { actionViewItemProvider: action => { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, undefined, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService); + const item = new CodiconActionViewItem(action, undefined, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService, this.accessibilityService); return item; } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index e21aba1d2ec85..38210f5785b40 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -368,7 +368,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD // output is already gone removedItems.push(key); } else { - const cellTop = this._list.getAbsoluteTopOfElement(value.cellInfo.diffElement); + const cellTop = this._list.getCellViewScrollTop(value.cellInfo.diffElement); const outputIndex = cell.outputsViewModels.indexOf(key); const outputOffset = value.cellInfo.diffElement.getOutputOffsetInCell(diffSide, outputIndex); updateItems.push({ @@ -872,10 +872,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } if (!activeWebview.insetMapping.has(output.source)) { - const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const cellTop = this._list.getCellViewScrollTop(cellDiffViewModel); await activeWebview.createOutput({ diffElement: cellDiffViewModel, cellHandle: cellViewModel.handle, cellId: cellViewModel.id, cellUri: cellViewModel.uri }, output, cellTop, getOffset()); } else { - const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const cellTop = this._list.getCellViewScrollTop(cellDiffViewModel); const outputIndex = cellViewModel.outputsViewModels.indexOf(output.source); const outputOffset = cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); activeWebview.updateScrollTops([{ @@ -927,7 +927,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return; } - const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const cellTop = this._list.getCellViewScrollTop(cellDiffViewModel); const outputIndex = cellViewModel.outputsViewModels.indexOf(displayOutput); const outputOffset = cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); activeWebview.updateScrollTops([{ diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts index d07f14414960a..7cff07828c400 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts @@ -29,6 +29,7 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { PixelRatio } from 'vs/base/browser/browser'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { fixedDiffEditorOptions, fixedEditorOptions } from 'vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { private readonly lineHeight: number; @@ -168,6 +169,7 @@ export class CellDiffSideBySideRenderer implements IListRenderer { if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, undefined, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService); + const item = new CodiconActionViewItem(action, undefined, this.keybindingService, this.notificationService, this.contextKeyService, this.themeService, this.contextMenuService, this.accessibilityService); return item; } @@ -322,7 +324,7 @@ export class NotebookTextDiffList extends WorkbenchList= this.length) { // this._getViewIndexUpperBound(element); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookEditorStickyScroll.css b/src/vs/workbench/contrib/notebook/browser/media/notebookEditorStickyScroll.css new file mode 100644 index 0000000000000..8107bfb128222 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookEditorStickyScroll.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .notebookOverlay .notebook-sticky-scroll-container { + display: none; + position: absolute; + background-color: var(--vscode-notebook-editorBackground); + z-index: var(--z-index-notebook-sticky-scroll); + width: 100%; + font-family: var(--notebook-cell-input-preview-font-family); +} +.monaco-workbench + .notebookOverlay + .notebook-sticky-scroll-container + .notebook-sticky-scroll-line { + padding-left: 12px; +} + +.monaco-workbench + .notebookOverlay + .notebook-sticky-scroll-container + .notebook-sticky-scroll-line:hover { + background-color: var(--vscode-editorStickyScrollHover-background); + cursor: pointer; +} + +.monaco-workbench + .notebookOverlay + .notebook-sticky-scroll-container + .notebook-shadow { + display: block; + top: 0; + left: 3px; + height: 3px; + width: 100%; + box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px inset; +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.css b/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.css rename to src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css b/src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css rename to src/vs/workbench/contrib/notebook/browser/media/notebookOutline.css diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 72cb92908d6a9..e461088658249 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -112,9 +112,10 @@ import { NotebookKernelHistoryService } from 'vs/workbench/contrib/notebook/brow import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; import { NotebookLoggingService } from 'vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl'; import product from 'vs/platform/product/common/product'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; -import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { runAccessibilityHelpAction } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp'; +import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { runAccessibilityHelpAction, showAccessibleOutput } from 'vs/workbench/contrib/notebook/browser/notebookAccessibility'; +import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; /*--------------------------------------------------------------------------------------------- */ @@ -689,6 +690,19 @@ class NotebookAccessibilityHelpContribution extends Disposable { } } +class NotebookAccessibleViewContribution extends Disposable { + static ID: 'chatAccessibleViewContribution'; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(100, 'notebook', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const editorService = accessor.get(IEditorService); + + return showAccessibleOutput(accessibleViewService, editorService); + }, NOTEBOOK_OUTPUT_FOCUSED)); + } +} + const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); @@ -698,6 +712,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(NotebookEditorManag workbenchContributionsRegistry.registerWorkbenchContribution(NotebookLanguageSelectorScoreRefine, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(SimpleNotebookWorkingCopyEditorHandler, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibilityHelpContribution, LifecyclePhase.Eventually); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibleViewContribution, LifecyclePhase.Eventually); registerSingleton(INotebookService, NotebookService, InstantiationType.Delayed); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl, InstantiationType.Delayed); @@ -847,6 +862,12 @@ configurationRegistry.registerConfiguration({ default: true, tags: ['notebookLayout'] }, + [NotebookSetting.stickyScroll]: { + description: nls.localize('notebook.stickyScroll.description', "Experimental. Control whether to render notebook Sticky Scroll headers in the notebook editor."), + type: 'boolean', + default: false, + tags: ['notebookLayout'] + }, [NotebookSetting.consolidatedOutputButton]: { description: nls.localize('notebook.consolidatedOutputButton.description', "Control whether outputs action should be rendered in the output toolbar."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts similarity index 59% rename from src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts rename to src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts index 717b6167614d7..8e94fe17ecd30 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts @@ -9,6 +9,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; export function getAccessibilityHelpText(accessor: ServicesAccessor): string { const keybindingService = accessor.get(IKeybindingService); @@ -46,7 +49,7 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi const accessibleViewService = accessor.get(IAccessibleViewService); const helpText = getAccessibilityHelpText(accessor); accessibleViewService.show({ - verbositySettingKey: 'notebook', + verbositySettingKey: AccessibilityVerbositySettingId.Notebook, provideContent: () => helpText, onClose: () => { editor.focus(); @@ -54,3 +57,67 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi options: { type: AccessibleViewType.HelpMenu, ariaLabel: 'Notebook accessibility help' } }); } + +export function showAccessibleOutput(accessibleViewService: IAccessibleViewService, editorService: IEditorService) { + const activePane = editorService.activeEditorPane; + const notebookEditor = getNotebookEditorFromEditorPane(activePane); + const notebookViewModel = notebookEditor?.getViewModel(); + const selections = notebookViewModel?.getSelections(); + const notebookDocument = notebookViewModel?.notebookDocument; + + if (!selections || !notebookDocument || !notebookEditor?.textModel) { + return false; + } + + const viewCell = notebookViewModel.viewCells[selections[0].start]; + let outputContent = ''; + const decoder = new TextDecoder(); + for (let i = 0; i < viewCell.outputsViewModels.length; i++) { + const outputViewModel = viewCell.outputsViewModels[i]; + const outputTextModel = viewCell.model.outputs[i]; + const [mimeTypes, pick] = outputViewModel.resolveMimeTypes(notebookEditor.textModel, undefined); + const mimeType = mimeTypes[pick].mimeType; + let buffer = outputTextModel.outputs.find(output => output.mime === mimeType); + + if (!buffer || mimeType.startsWith('image')) { + buffer = outputTextModel.outputs.find(output => !output.mime.startsWith('image')); + } + + let text = `${mimeType}`; // default in case we can't get the text value for some reason. + if (buffer) { + const charLimit = 100_000; + text = decoder.decode(buffer.data.slice(0, charLimit).buffer); + + if (buffer.data.byteLength > charLimit) { + text = text + '...(truncated)'; + } + + if (mimeType.endsWith('error')) { + text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); + } + } + + const index = viewCell.outputsViewModels.length > 1 + ? `Cell output ${i + 1} of ${viewCell.outputsViewModels.length}\n` + : ''; + outputContent = outputContent.concat(`${index}${text}\n`); + } + + if (!outputContent) { + return false; + } + + accessibleViewService.show({ + verbositySettingKey: AccessibilityVerbositySettingId.Notebook, + provideContent(): string { return outputContent; }, + onClose() { + notebookEditor?.setFocus(selections[0]); + activePane?.focus(); + }, + options: { + ariaLabel: localize('NotebookCellOutputAccessibleView', "Notebook Cell Output Accessible View"), + type: AccessibleViewType.View + } + }); + return true; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 5166e31799152..71bd138fdf85c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -476,7 +476,7 @@ export interface INotebookEditor { setFocus(focus: ICellRange): void; getId(): string; - _getViewModel(): INotebookViewModel | undefined; + getViewModel(): INotebookViewModel | undefined; hasModel(): this is IActiveNotebookEditor; dispose(): void; getDomNode(): HTMLElement; @@ -566,6 +566,11 @@ export interface INotebookEditor { */ removeClassName(className: string): void; + /** + * Set scrollTop value of the notebook editor. + */ + setScrollTop(scrollTop: number): void; + /** * The range will be revealed with as little scrolling as possible. */ @@ -684,7 +689,7 @@ export interface INotebookEditor { } export interface IActiveNotebookEditor extends INotebookEditor { - _getViewModel(): INotebookViewModel; + getViewModel(): INotebookViewModel; textModel: NotebookTextModel; getFocus(): ICellRange; cellAt(index: number): ICellViewModel; @@ -730,7 +735,7 @@ export interface INotebookEditorDelegate extends INotebookEditor { } export interface IActiveNotebookEditorDelegate extends INotebookEditorDelegate { - _getViewModel(): INotebookViewModel; + getViewModel(): INotebookViewModel; textModel: NotebookTextModel; getFocus(): ICellRange; cellAt(index: number): ICellViewModel; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 71f9504ed238a..c1d0acf52f34c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -12,6 +12,10 @@ import 'vs/css!./media/notebookToolbar'; import 'vs/css!./media/notebookDnd'; import 'vs/css!./media/notebookFolding'; import 'vs/css!./media/notebookCellOutput'; +import 'vs/css!./media/notebookEditorStickyScroll'; +import 'vs/css!./media/notebookKernelActionViewItem'; +import 'vs/css!./media/notebookOutline'; + import { PixelRatio } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -90,8 +94,11 @@ import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/no import { Schemas } from 'vs/base/common/network'; import { DropIntoEditorController } from 'vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController'; import { CopyPasteController } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController'; +import { NotebookStickyScroll } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll'; +import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; const $ = DOM.$; @@ -171,6 +178,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private _overlayContainer!: HTMLElement; private _notebookTopToolbarContainer!: HTMLElement; private _notebookTopToolbar!: NotebookEditorWorkbenchToolbar; + private _notebookStickyScrollContainer!: HTMLElement; private _notebookOverviewRulerContainer!: HTMLElement; private _notebookOverviewRuler!: NotebookOverviewRuler; private _body!: HTMLElement; @@ -261,6 +269,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD public readonly scopedContextKeyService: IContextKeyService; private readonly instantiationService: IInstantiationService; private readonly _notebookOptions: NotebookOptions; + public readonly _notebookOutline: NotebookCellOutlineProvider; private _currentProgress: IProgressRunner | undefined; @@ -315,6 +324,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._register(this.instantiationService.createInstance(NotebookEditorContextKeys, this)); + this._notebookOutline = this.instantiationService.createInstance(NotebookCellOutlineProvider, this, OutlineTarget.QuickPick); + this._register(notebookKernelService.onDidChangeSelectedNotebooks(e => { if (isEqual(e.notebook, this.viewModel?.uri)) { this._loadKernelPreloads(); @@ -442,7 +453,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return this._uuid; } - _getViewModel(): NotebookViewModel | undefined { + getViewModel(): NotebookViewModel | undefined { return this.viewModel; } @@ -565,6 +576,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._notebookTopToolbarContainer.classList.add('notebook-toolbar-container'); this._notebookTopToolbarContainer.style.display = 'none'; DOM.append(parent, this._notebookTopToolbarContainer); + + this._notebookStickyScrollContainer = document.createElement('div'); + this._notebookStickyScrollContainer.classList.add('notebook-sticky-scroll-container'); + DOM.append(parent, this._notebookStickyScrollContainer); + this._body = document.createElement('div'); DOM.append(parent, this._body); @@ -999,6 +1015,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); this._registerNotebookActionsToolbar(); + this._registerNotebookStickyScroll(); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Notebook)) { @@ -1028,6 +1045,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); } + private _registerNotebookStickyScroll() { + this._register(this.instantiationService.createInstance(NotebookStickyScroll, this._notebookStickyScrollContainer, this, this._notebookOutline, this._list)); + } + private _updateOutputRenderers() { if (!this.viewModel || !this._webview) { return; @@ -2017,13 +2038,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } getAbsoluteTopOfElement(cell: ICellViewModel) { - return this._list.getAbsoluteTopOfElement(cell); + return this._list.getCellViewScrollTop(cell); } scrollToBottom() { this._list.scrollToBottom(); } + setScrollTop(scrollTop: number): void { + this._list.scrollTop = scrollTop; + } + revealCellRangeInView(range: ICellRange) { return this._list.revealCellsInView(range); } @@ -2608,7 +2633,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); const top = !!webviewTop ? (0 - webviewTop) : 0; - const cellTop = this._list.getAbsoluteTopOfElement(cell); + const cellTop = this._list.getCellViewScrollTop(cell); await this._webview.showMarkupPreview({ mime: cell.mime, cellHandle: cell.handle, @@ -2702,7 +2727,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); const top = !!webviewTop ? (0 - webviewTop) : 0; - const cellTop = this._list.getAbsoluteTopOfElement(cell) + top; + const cellTop = this._list.getCellViewScrollTop(cell) + top; const existingOutput = this._webview.insetMapping.get(output.source); if (!existingOutput @@ -2760,7 +2785,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); const top = !!webviewTop ? (0 - webviewTop) : 0; - const cellTop = this._list.getAbsoluteTopOfElement(cell) + top; + const cellTop = this._list.getCellViewScrollTop(cell) + top; this._webview.updateOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); }); } @@ -2868,7 +2893,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD removedItems.push(key); } - const cellTop = this._list.getAbsoluteTopOfElement(cell); + const cellTop = this._list.getCellViewScrollTop(cell); const outputIndex = cell.outputsViewModels.indexOf(key); const outputOffset = cell.getOutputOffset(outputIndex); updateItems.push({ @@ -2886,7 +2911,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD for (const cellId of this._webview.markupPreviewMapping.keys()) { const cell = this.viewModel?.viewCells.find(cell => cell.id === cellId); if (cell) { - const cellTop = this._list.getAbsoluteTopOfElement(cell); + const cellTop = this._list.getCellViewScrollTop(cell); // markdownUpdateItems.push({ id: cellId, top: cellTop }); markdownUpdateItems.push({ id: cellId, top: cellTop + top }); } @@ -3089,6 +3114,7 @@ registerZIndex(ZIndex.Base, 28, 'notebook-cell-bottom-toolbar-container'); registerZIndex(ZIndex.Base, 29, 'notebook-run-button-container'); registerZIndex(ZIndex.Base, 29, 'notebook-input-collapse-condicon'); registerZIndex(ZIndex.Base, 30, 'notebook-cell-output-toolbar'); +registerZIndex(ZIndex.Base, 31, 'notebook-sticky-scroll'); registerZIndex(ZIndex.Sash, 1, 'notebook-cell-expand-part-button'); registerZIndex(ZIndex.Sash, 2, 'notebook-cell-toolbar'); registerZIndex(ZIndex.Sash, 3, 'notebook-cell-toolbar-dropdown-active'); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 2140b0286ae98..55f6fff83baa3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -61,6 +61,7 @@ export interface NotebookLayoutConfiguration { insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'; insertToolbarAlignment: 'left' | 'center'; globalToolbar: boolean; + stickyScroll: boolean; consolidatedOutputButton: boolean; consolidatedRunButton: boolean; showFoldingControls: 'always' | 'never' | 'mouseover'; @@ -89,6 +90,7 @@ export interface NotebookOptionsChangeEvent { readonly insertToolbarPosition?: boolean; readonly insertToolbarAlignment?: boolean; readonly globalToolbar?: boolean; + readonly stickyScroll?: boolean; readonly showFoldingControls?: boolean; readonly consolidatedOutputButton?: boolean; readonly consolidatedRunButton?: boolean; @@ -134,11 +136,12 @@ export class NotebookOptions extends Disposable { private readonly configurationService: IConfigurationService, private readonly notebookExecutionStateService: INotebookExecutionStateService, private isReadonly: boolean, - private readonly overrides?: { cellToolbarInteraction: string; globalToolbar: boolean; dragAndDropEnabled: boolean } + private readonly overrides?: { cellToolbarInteraction: string; globalToolbar: boolean; stickyScroll: boolean; dragAndDropEnabled: boolean } ) { super(); const showCellStatusBar = this.configurationService.getValue(NotebookSetting.showCellStatusBar); const globalToolbar = overrides?.globalToolbar ?? this.configurationService.getValue(NotebookSetting.globalToolbar) ?? true; + const stickyScroll = overrides?.stickyScroll ?? this.configurationService.getValue(NotebookSetting.stickyScroll) ?? false; const consolidatedOutputButton = this.configurationService.getValue(NotebookSetting.consolidatedOutputButton) ?? true; const consolidatedRunButton = this.configurationService.getValue(NotebookSetting.consolidatedRunButton) ?? false; const dragAndDropEnabled = overrides?.dragAndDropEnabled ?? this.configurationService.getValue(NotebookSetting.dragAndDropEnabled) ?? true; @@ -213,6 +216,7 @@ export class NotebookOptions extends Disposable { collapsedIndicatorHeight: 28, showCellStatusBar, globalToolbar, + stickyScroll, consolidatedOutputButton, consolidatedRunButton, dragAndDropEnabled, @@ -335,6 +339,7 @@ export class NotebookOptions extends Disposable { const insertToolbarPosition = e.affectsConfiguration(NotebookSetting.insertToolbarLocation); const insertToolbarAlignment = e.affectsConfiguration(NotebookSetting.experimentalInsertToolbarAlignment); const globalToolbar = e.affectsConfiguration(NotebookSetting.globalToolbar); + const stickyScroll = e.affectsConfiguration(NotebookSetting.stickyScroll); const consolidatedOutputButton = e.affectsConfiguration(NotebookSetting.consolidatedOutputButton); const consolidatedRunButton = e.affectsConfiguration(NotebookSetting.consolidatedRunButton); const showFoldingControls = e.affectsConfiguration(NotebookSetting.showFoldingControls); @@ -359,6 +364,7 @@ export class NotebookOptions extends Disposable { && !insertToolbarPosition && !insertToolbarAlignment && !globalToolbar + && !stickyScroll && !consolidatedOutputButton && !consolidatedRunButton && !showFoldingControls @@ -414,6 +420,10 @@ export class NotebookOptions extends Disposable { configuration.globalToolbar = this.configurationService.getValue(NotebookSetting.globalToolbar) ?? true; } + if (stickyScroll && this.overrides?.stickyScroll === undefined) { + configuration.stickyScroll = this.configurationService.getValue(NotebookSetting.stickyScroll) ?? false; + } + if (consolidatedOutputButton) { configuration.consolidatedOutputButton = this.configurationService.getValue(NotebookSetting.consolidatedOutputButton) ?? true; } @@ -479,6 +489,7 @@ export class NotebookOptions extends Disposable { insertToolbarPosition, insertToolbarAlignment, globalToolbar, + stickyScroll, showFoldingControls, consolidatedOutputButton, consolidatedRunButton, diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts index 007ed9dc801c4..5531da7b673db 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -140,7 +140,7 @@ export class CellDragAndDropController extends Disposable { return undefined; } - const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); + const cellTop = this.list.getCellViewScrollTop(draggedOverCell); const cellHeight = this.list.elementHeight(draggedOverCell); const dragPosInElement = dragOffset - cellTop; @@ -228,7 +228,7 @@ export class CellDragAndDropController extends Disposable { } private _dropImpl(draggedCell: ICellViewModel, dropDirection: 'above' | 'below', ctx: { ctrlKey: boolean; altKey: boolean }, draggedOverCell: ICellViewModel) { - const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); + const cellTop = this.list.getCellViewScrollTop(draggedOverCell); const cellHeight = this.list.elementHeight(draggedOverCell); const insertionIndicatorAbsolutePos = dropDirection === 'above' ? cellTop : cellTop + cellHeight; const { bottomToolbarGap } = this.notebookEditor.notebookOptions.computeBottomToolbarDimensions(this.notebookEditor.textModel?.viewType); @@ -358,7 +358,7 @@ export class CellDragAndDropController extends Disposable { const target = this.list.elementAt(dragOffsetY); if (target && target !== cell) { - const cellTop = this.list.getAbsoluteTopOfElement(target); + const cellTop = this.list.getCellViewScrollTop(target); const cellHeight = this.list.elementHeight(target); const dropDirection = this.getExplicitDragDropDirection(dragOffsetY, cellTop, cellHeight); @@ -400,7 +400,7 @@ export class CellDragAndDropController extends Disposable { return; } - const cellTop = this.list.getAbsoluteTopOfElement(target); + const cellTop = this.list.getCellViewScrollTop(target); const cellHeight = this.list.elementHeight(target); const dropDirection = this.getExplicitDragDropDirection(ctx.dragOffsetY, cellTop, cellHeight); this._dropImpl(cell, dropDirection, ctx, target); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts index 227be9967a271..f425ea10dcbfe 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts @@ -15,8 +15,7 @@ export function registerCellToolbarStickyScroll(notebookEditor: INotebookEditor, if (cell.isInputCollapsed) { element.style.top = ''; } else { - const scrollPadding = notebookEditor.notebookOptions.computeTopInsertToolbarHeight(notebookEditor.textModel?.viewType); - const scrollTop = notebookEditor.scrollTop - scrollPadding; + const scrollTop = notebookEditor.scrollTop; const elementTop = notebookEditor.getAbsoluteTopOfElement(cell); const diff = scrollTop - elementTop + extraOffset; const maxTop = cell.layoutInfo.editorHeight + cell.layoutInfo.statusBarHeight - 45; // subtract roughly the height of the execution order label plus padding 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 d5dff6359c7d2..2fe72e05af8b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts @@ -33,8 +33,8 @@ export class FoldedCellHint extends CellContentPart { if (element.isInputCollapsed || element.getEditState() === CellEditState.Editing) { DOM.hide(this._container); } else if (element.foldingState === CellFoldingState.Collapsed) { - const idx = this._notebookEditor._getViewModel().getCellIndex(element); - const length = this._notebookEditor._getViewModel().getFoldedLength(idx); + 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.show(this._container); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 95e6b840346aa..0da4dd257425d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -135,8 +135,6 @@ export class NotebookCellList extends WorkbenchList implements ID private _isInLayout: boolean = false; - private readonly _viewContext: ViewContext; - private _webviewElement: FastDomNode | null = null; get webviewElement() { @@ -161,7 +159,6 @@ export class NotebookCellList extends WorkbenchList implements ID ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, configurationService, instantiationService); NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); - this._viewContext = viewContext; this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { this._previousFocusedElements.forEach(element => { @@ -686,6 +683,26 @@ export class NotebookCellList extends WorkbenchList implements ID this.setSelection(indices); } + getCellViewScrollTop(cell: ICellViewModel) { + const index = this._getViewIndexUpperBound(cell); + if (index === undefined || index < 0 || index >= this.length) { + throw new ListError(this.listUser, `Invalid index ${index}`); + } + + return this.view.elementTop(index); + } + + getCellViewScrollBottom(cell: ICellViewModel) { + const index = this._getViewIndexUpperBound(cell); + if (index === undefined || index < 0 || index >= this.length) { + throw new ListError(this.listUser, `Invalid index ${index}`); + } + + const top = this.view.elementTop(index); + const height = this.view.elementHeight(index); + return top + height; + } + override setFocus(indexes: number[], browserEvent?: UIEvent, ignoreTextModelUpdate?: boolean): void { if (ignoreTextModelUpdate) { super.setFocus(indexes, browserEvent); @@ -806,9 +823,8 @@ export class NotebookCellList extends WorkbenchList implements ID const scrollHeight = this.view.scrollHeight; const scrollTop = this.getViewScrollTop(); const wrapperBottom = this.getViewScrollBottom(); - const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType); - this.view.setScrollTop(scrollHeight - (wrapperBottom - scrollTop) - topInsertToolbarHeight); + this.view.setScrollTop(scrollHeight - (wrapperBottom - scrollTop)); } //#region Reveal Cell synchronously @@ -1111,16 +1127,6 @@ export class NotebookCellList extends WorkbenchList implements ID this.view.domNode.focus(); } - getAbsoluteTopOfElement(element: ICellViewModel): number { - const index = this._getViewIndexUpperBound(element); - if (index === undefined || index < 0 || index >= this.length) { - this._getViewIndexUpperBound(element); - throw new ListError(this.listUser, `Invalid index ${index}`); - } - - return this.view.elementTop(index); - } - triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { this.view.delegateScrollFromMouseWheelEvent(browserEvent); } @@ -1216,8 +1222,7 @@ export class NotebookCellList extends WorkbenchList implements ID } getViewScrollBottom() { - const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType); - return this.getViewScrollTop() + this.view.renderHeight - topInsertToolbarHeight; + return this.getViewScrollTop() + this.view.renderHeight; } setCellEditorSelection(cell: ICellViewModel, range: Range) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 0276ee9e10e6f..68a9cc8af769f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -54,6 +54,8 @@ export interface INotebookCellList { attachViewModel(viewModel: NotebookViewModel): void; attachWebview(element: HTMLElement): void; clear(): void; + getCellViewScrollTop(cell: ICellViewModel): number; + getCellViewScrollBottom(cell: ICellViewModel): number; getViewIndex(cell: ICellViewModel): number | undefined; getViewIndex2(modelIndex: number): number | undefined; getModelIndex(cell: CellViewModel): number | undefined; @@ -72,7 +74,6 @@ export interface INotebookCellList { setHiddenAreas(_ranges: ICellRange[], triggerViewUpdate: boolean): boolean; domElementOfElement(element: ICellViewModel): HTMLElement | null; focusView(): void; - getAbsoluteTopOfElement(element: ICellViewModel): number; triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void; updateElementHeight2(element: ICellViewModel, size: number, anchorElementIndex?: number | null): void; domFocus(): void; 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 189bd55748887..fe247bf9a854e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -767,7 +767,8 @@ export class BackLayerWebView extends Themable { const uri = URI.parse(data.href); this._handleNotebookCellResource(uri); } else if (!/^[\w\-]+:/.test(data.href)) { - this._handleResourceOpening(data.href); + // Uri without scheme, such as a file path + this._handleResourceOpening(tryDecodeURIComponent(data.href)); } else { // uri with scheme if (osPath.isAbsolute(data.href)) { @@ -1795,3 +1796,11 @@ function getTokenizationCss() { const tokenizationCss = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; return tokenizationCss; } + +function tryDecodeURIComponent(uri: string) { + try { + return decodeURIComponent(uri); + } catch { + return uri; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts new file mode 100644 index 0000000000000..a0791175d4247 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts @@ -0,0 +1,397 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, MutableDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IActiveNotebookEditor, ICellViewModel, INotebookEditor, INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { executingStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { getMarkdownHeadersInCell } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; + +export interface IOutlineMarkerInfo { + readonly count: number; + readonly topSev: MarkerSeverity; +} + +export class OutlineEntry { + private _children: OutlineEntry[] = []; + private _parent: OutlineEntry | undefined; + private _markerInfo: IOutlineMarkerInfo | undefined; + + get icon(): ThemeIcon { + return this.isExecuting && this.isPaused ? executingStateIcon : + this.isExecuting ? ThemeIcon.modify(executingStateIcon, 'spin') : + this.cell.cellKind === CellKind.Markup ? Codicon.markdown : Codicon.code; + } + + constructor( + readonly index: number, + readonly level: number, + readonly cell: ICellViewModel, + readonly label: string, + readonly isExecuting: boolean, + readonly isPaused: boolean + ) { } + + addChild(entry: OutlineEntry) { + this._children.push(entry); + entry._parent = this; + } + + get parent(): OutlineEntry | undefined { + return this._parent; + } + + get children(): Iterable { + return this._children; + } + + get markerInfo(): IOutlineMarkerInfo | undefined { + return this._markerInfo; + } + + updateMarkers(markerService: IMarkerService): void { + if (this.cell.cellKind === CellKind.Code) { + // a code cell can have marker + const marker = markerService.read({ resource: this.cell.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); + if (marker.length === 0) { + this._markerInfo = undefined; + } else { + const topSev = marker.find(a => a.severity === MarkerSeverity.Error)?.severity ?? MarkerSeverity.Warning; + this._markerInfo = { topSev, count: marker.length }; + } + } else { + // a markdown cell can inherit markers from its children + let topChild: MarkerSeverity | undefined; + for (const child of this.children) { + child.updateMarkers(markerService); + if (child.markerInfo) { + topChild = !topChild ? child.markerInfo.topSev : Math.max(child.markerInfo.topSev, topChild); + } + } + this._markerInfo = topChild && { topSev: topChild, count: 0 }; + } + } + + clearMarkers(): void { + this._markerInfo = undefined; + for (const child of this.children) { + child.clearMarkers(); + } + } + + find(cell: ICellViewModel, parents: OutlineEntry[]): OutlineEntry | undefined { + if (cell.id === this.cell.id) { + return this; + } + parents.push(this); + for (const child of this.children) { + const result = child.find(cell, parents); + if (result) { + return result; + } + } + parents.pop(); + return undefined; + } + + asFlatList(bucket: OutlineEntry[]): void { + bucket.push(this); + for (const child of this.children) { + child.asFlatList(bucket); + } + } +} + + +export class NotebookCellOutlineProvider { + private readonly _dispoables = new DisposableStore(); + private readonly _onDidChange = new Emitter(); + + readonly onDidChange: Event = this._onDidChange.event; + + private _uri: URI | undefined; + private _entries: OutlineEntry[] = []; + get entries(): OutlineEntry[] { + return this._entries; + } + + private _activeEntry?: OutlineEntry; + private readonly _entriesDisposables = new DisposableStore(); + + readonly outlineKind = 'notebookCells'; + + get activeElement(): OutlineEntry | undefined { + return this._activeEntry; + } + + constructor( + private readonly _editor: INotebookEditor, + private readonly _target: OutlineTarget, + @IThemeService themeService: IThemeService, + @IEditorService _editorService: IEditorService, + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, + ) { + const selectionListener = new MutableDisposable(); + this._dispoables.add(selectionListener); + + selectionListener.value = combinedDisposable( + Event.debounce( + _editor.onDidChangeSelection, + (last, _current) => last, + 200 + )(this._recomputeActive, this), + Event.debounce( + _editor.onDidChangeViewCells, + (last, _current) => last ?? _current, + 200 + )(this._recomputeState, this) + ); + + this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('notebook.outline.showCodeCells')) { + this._recomputeState(); + } + })); + + this._dispoables.add(themeService.onDidFileIconThemeChange(() => { + this._onDidChange.fire({}); + })); + + this._dispoables.add(_notebookExecutionStateService.onDidChangeExecution(e => { + if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) { + this._recomputeState(); + } + })); + + this._recomputeState(); + } + + dispose(): void { + // selectionListener.clear(); + this._dispoables.dispose(); + } + + init(): void { + this._recomputeState(); + } + + private _recomputeState(): void { + this._entriesDisposables.clear(); + this._activeEntry = undefined; + this._entries.length = 0; + this._uri = undefined; + + if (!this._editor.hasModel()) { + return; + } + + this._uri = this._editor.textModel.uri; + + const notebookEditorWidget: IActiveNotebookEditor = this._editor; + + if (notebookEditorWidget.getLength() === 0) { + return; + } + + let includeCodeCells = true; + if (this._target === OutlineTarget.OutlinePane) { + includeCodeCells = this._configurationService.getValue('notebook.outline.showCodeCells'); + } else if (this._target === OutlineTarget.Breadcrumbs) { + includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); + } + + const focusedCellIndex = notebookEditorWidget.getFocus().start; + const focused = notebookEditorWidget.cellAt(focusedCellIndex)?.handle; + const entries: OutlineEntry[] = []; + + for (let i = 0; i < notebookEditorWidget.getLength(); i++) { + const cell = notebookEditorWidget.cellAt(i); + const isMarkdown = cell.cellKind === CellKind.Markup; + if (!isMarkdown && !includeCodeCells) { + continue; + } + + // cap the amount of characters that we look at and use the following logic + // - for MD prefer headings (each header is an entry) + // - otherwise use the first none-empty line of the cell (MD or code) + let content = this._getCellFirstNonEmptyLine(cell); + let hasHeader = false; + + if (isMarkdown) { + const fullContent = cell.getText().substring(0, 10_000); + for (const { depth, text } of getMarkdownHeadersInCell(fullContent)) { + hasHeader = true; + entries.push(new OutlineEntry(entries.length, depth, cell, text, false, false)); + } + + if (!hasHeader) { + // no markdown syntax headers, try to find html tags + const match = fullContent.match(/(.*)<\/h\1>/i); + if (match) { + hasHeader = true; + const level = parseInt(match[1]); + const text = match[2].trim(); + entries.push(new OutlineEntry(entries.length, level, cell, text, false, false)); + } + } + + if (!hasHeader) { + content = renderMarkdownAsPlaintext({ value: content }); + } + } + + if (!hasHeader) { + let preview = content.trim(); + if (preview.length === 0) { + // empty or just whitespace + preview = localize('empty', "empty cell"); + } + + const exeState = !isMarkdown && this._notebookExecutionStateService.getCellExecution(cell.uri); + entries.push(new OutlineEntry(entries.length, 7, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); + } + + if (cell.handle === focused) { + this._activeEntry = entries[entries.length - 1]; + } + + // send an event whenever any of the cells change + this._entriesDisposables.add(cell.model.onDidChangeContent(() => { + this._recomputeState(); + this._onDidChange.fire({}); + })); + } + + // build a tree from the list of entries + if (entries.length > 0) { + const result: OutlineEntry[] = [entries[0]]; + const parentStack: OutlineEntry[] = [entries[0]]; + + for (let i = 1; i < entries.length; i++) { + const entry = entries[i]; + + while (true) { + const len = parentStack.length; + if (len === 0) { + // root node + result.push(entry); + parentStack.push(entry); + break; + + } else { + const parentCandidate = parentStack[len - 1]; + if (parentCandidate.level < entry.level) { + parentCandidate.addChild(entry); + parentStack.push(entry); + break; + } else { + parentStack.pop(); + } + } + } + } + this._entries = result; + } + + // feature: show markers with each cell + const markerServiceListener = new MutableDisposable(); + this._entriesDisposables.add(markerServiceListener); + const updateMarkerUpdater = () => { + if (notebookEditorWidget.isDisposed) { + return; + } + + const doUpdateMarker = (clear: boolean) => { + for (const entry of this._entries) { + if (clear) { + entry.clearMarkers(); + } else { + entry.updateMarkers(this._markerService); + } + } + }; + if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + markerServiceListener.value = this._markerService.onMarkerChanged(e => { + if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) { + doUpdateMarker(false); + this._onDidChange.fire({}); + } + }); + doUpdateMarker(false); + } else { + markerServiceListener.clear(); + doUpdateMarker(true); + } + }; + updateMarkerUpdater(); + this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + updateMarkerUpdater(); + this._onDidChange.fire({}); + } + })); + + this._onDidChange.fire({}); + } + + private _recomputeActive(): void { + let newActive: OutlineEntry | undefined; + const notebookEditorWidget = this._editor; + + if (notebookEditorWidget) {//TODO don't check for widget, only here if we do have + if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) { + const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start); + if (cell) { + for (const entry of this._entries) { + newActive = entry.find(cell, []); + if (newActive) { + break; + } + } + } + } + } + if (newActive !== this._activeEntry) { + this._activeEntry = newActive; + this._onDidChange.fire({ affectOnlyActiveElement: true }); + } + } + + private _getCellFirstNonEmptyLine(cell: ICellViewModel) { + const textBuffer = cell.textBuffer; + for (let i = 0; i < textBuffer.getLineCount(); i++) { + const firstNonWhitespace = textBuffer.getLineFirstNonWhitespaceColumn(i + 1); + const lineLength = textBuffer.getLineLength(i + 1); + if (firstNonWhitespace < lineLength) { + return textBuffer.getLineContent(i + 1); + } + } + + return cell.getText().substring(0, 10_000); + } + + get isEmpty(): boolean { + return this._entries.length === 0; + } + + get uri() { + return this._uri; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts new file mode 100644 index 0000000000000..1855cc0413f48 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -0,0 +1,442 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as DOM from 'vs/base/browser/dom'; +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 { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; +import { NotebookCellOutlineProvider, OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; + +export class ToggleNotebookStickyScroll extends Action2 { + + constructor() { + super({ + id: 'notebook.action.toggleNotebookStickyScroll', + title: { + value: localize('toggleStickyScroll', "Toggle Notebook Sticky Scroll"), + mnemonicTitle: localize({ key: 'mitoggleStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), + original: '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); + } +} + +class NotebookStickyLine extends Disposable { + constructor( + public readonly element: HTMLElement, + public readonly entry: OutlineEntry, + public readonly notebookEditor: INotebookEditor, + ) { + super(); + this._register(DOM.addDisposableListener(this.element, DOM.EventType.CLICK, (e) => { + console.log('click on sticky line'); + this.focusCell(); + })); + } + + private focusCell() { + this.notebookEditor.focusNotebookCell(this.entry.cell, 'container'); + const cellScrollTop = this.notebookEditor.getAbsoluteTopOfElement(this.entry.cell); + const parentCount = this.getParentCount(); + // 1.1 addresses visible cell padding, to make sure we don't focus md cell and also render its sticky line + this.notebookEditor.setScrollTop(cellScrollTop - (parentCount + 1.1) * 22); + } + + private getParentCount() { + let count = 0; + let entry = this.entry; + while (entry.parent) { + count++; + entry = entry.parent; + } + return count; + } + +} + + +export class NotebookStickyScroll extends Disposable { + private readonly _disposables = new DisposableStore(); + private currentStickyLines = new Map(); + + getDomNode(): HTMLElement { + return this.domNode; + } + + constructor( + private readonly domNode: HTMLElement, + private readonly notebookEditor: INotebookEditor, + private readonly notebookOutline: NotebookCellOutlineProvider, + private readonly notebookCellList: INotebookCellList, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + ) { + super(); + + if (this.notebookEditor.notebookOptions.getLayoutConfiguration().stickyScroll) { + this.init(); + } + + this._register(this.notebookEditor.notebookOptions.onDidChangeOptions((e) => { + if (e.stickyScroll) { + this.updateConfig(); + } + if (e.globalToolbar) { + this.setTop(); + } + })); + + this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.CONTEXT_MENU, async (event: MouseEvent) => { + this.onContextMenu(event); + })); + } + + private onContextMenu(e: MouseEvent) { + const event = new StandardMouseEvent(e); + this._contextMenuService.showContextMenu({ + menuId: MenuId.NotebookStickyScrollContext, + getAnchor: () => event, + }); + } + + private updateConfig() { + if (this.notebookEditor.notebookOptions.getLayoutConfiguration().stickyScroll) { + this.init(); + } else { + this._disposables.clear(); + DOM.clearNode(this.domNode); + this.updateDisplay(); + } + } + + private setTop() { + if (this.notebookEditor.notebookOptions.getLayoutConfiguration().globalToolbar) { + this.domNode.style.top = '26px'; + } else { + this.domNode.style.top = '0px'; + } + } + + private init() { + this.notebookOutline.init(); + this.initializeContent(); + + this._disposables.add(this.notebookOutline.onDidChange(() => { + this.updateContent(); + })); + + this._disposables.add(this.notebookEditor.onDidAttachViewModel(() => { + this.notebookOutline.init(); + this.initializeContent(); + })); + + this._disposables.add(this.notebookEditor.onDidScroll(() => { + this.updateContent(); + })); + } + + private getVisibleOutlineEntry(visibleIndex: number): OutlineEntry | undefined { + let left = 0; + let right = this.notebookOutline.entries.length - 1; + let bucket = -1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + if (this.notebookOutline.entries[mid].index < visibleIndex) { + bucket = mid; + left = mid + 1; + } else { + right = mid - 1; + } + } + + if (bucket !== -1) { + const rootEntry = this.notebookOutline.entries[bucket]; + const flatList: OutlineEntry[] = []; + rootEntry.asFlatList(flatList); + return flatList.find(entry => entry.index === visibleIndex); + } + return undefined; + } + + private initializeContent() { + + // find last code cell of section, store bottom scroll position in sectionBottom + const visibleRange = this.notebookEditor.visibleRanges[0]; + if (!visibleRange) { + return; + } + + DOM.clearNode(this.domNode); + const editorScrollTop = this.notebookEditor.scrollTop; + + let trackedEntry = undefined; + let sectionBottom = 0; + for (let i = visibleRange.start; i < visibleRange.end; i++) { + if (i === 0) { // don't show headers when you're viewing the top cell + this.updateDisplay(); + this.currentStickyLines = new Map(); + return; + } + const cell = this.notebookEditor.cellAt(i); + if (!cell) { + return; + } + if (cell.cellKind === CellKind.Markup) { + continue; + } + + // if we are here, the cell is a code cell. + // check next cell, if markdown, that means this is the end of the section + const nextCell = this.notebookEditor.cellAt(i + 1); + if (nextCell) { + if (nextCell.cellKind === CellKind.Markup) { + // this is the end of the section + // store the bottom scroll position of this cell + sectionBottom = this.notebookCellList.getCellViewScrollBottom(cell); + // compute sticky scroll height + const entry = this.getVisibleOutlineEntry(i); + if (!entry) { + return; + } + // using 22 instead of stickyscrollheight, as we don't necessarily render each line. 22 starts rendering sticky when we have space for at least 1 of them + const newStickyHeight = this.computeStickyHeight(entry!); + if (editorScrollTop + newStickyHeight < sectionBottom) { + trackedEntry = entry; + break; + } else { + // if (editorScrollTop + stickyScrollHeight > sectionBottom), then continue to next section + continue; + } + } + } else { + // there is no next cell, so use the bottom of the editor as the sectionBottom, using scrolltop + height + sectionBottom = this.notebookEditor.scrollTop + this.notebookEditor.getLayoutInfo().scrollHeight; + trackedEntry = this.getVisibleOutlineEntry(i); + break; + } + } // cell loop close + + // ------------------------------------------------------------------------------------- + // we now know the cell which the sticky is determined by, and the sectionBottom value to determine how many sticky lines to render + // compute the space available for sticky lines, and render sticky lines + + const linesToRender = Math.floor((sectionBottom - editorScrollTop) / 22); + let newMap: Map | undefined = new Map(); + newMap = this.renderStickyLines(trackedEntry?.parent, this.domNode, linesToRender, newMap); + if (!newMap) { + newMap = new Map(); + } + this.currentStickyLines = newMap; + this.updateDisplay(); + } + + + private updateContent() { + // find first code cell in visible range. this marks the start of the first section + // find the last code cell in the first section of the visible range, store the bottom scroll position in a const sectionBottom + // compute sticky scroll height, and check if editorScrolltop + stickyScrollHeight < sectionBottom + // if that condition is true, break out of the loop with that cell as the tracked cell + // if that condition is false, continue to next cell + + DOM.clearNode(this.domNode); + // iterate over current map and dispose each notebookstickyline + this.currentStickyLines.forEach((value) => { + value.dispose(); + }); + + const editorScrollTop = this.notebookEditor.scrollTop; + + // find last code cell of section, store bottom scroll position in sectionBottom + const visibleRange = this.notebookEditor.visibleRanges[0]; + if (!visibleRange) { + this.updateDisplay(); + this.currentStickyLines = new Map(); + return; + } + + let trackedEntry = undefined; + let sectionBottom = 0; + for (let i = visibleRange.start; i < visibleRange.end; i++) { + if (i === 0) { // don't show headers when you're viewing the top cell + this.updateDisplay(); + this.currentStickyLines = new Map(); + return; + } + const cell = this.notebookEditor.cellAt(i); + if (!cell) { + return; + } + if (cell.cellKind === CellKind.Markup) { + continue; + } + + // if we are here, the cell is a code cell. + // check next cell, if markdown, that means this is the end of the section + const nextCell = this.notebookEditor.cellAt(i + 1); + if (nextCell) { + if (nextCell.cellKind === CellKind.Markup) { + // this is the end of the section + // store the bottom scroll position of this cell + sectionBottom = this.notebookCellList.getCellViewScrollBottom(cell); + // compute sticky scroll height + const entry = this.getVisibleOutlineEntry(i); + if (!entry) { + return; + } + // check if we can render this section of sticky + const currentSectionStickyHeight = this.computeStickyHeight(entry!); + if (editorScrollTop + currentSectionStickyHeight < sectionBottom) { + const linesToRender = Math.floor((sectionBottom - editorScrollTop) / 22); + let newMap: Map | undefined = new Map(); + newMap = this.renderStickyLines(entry?.parent, this.domNode, linesToRender, newMap); + if (!newMap) { + newMap = new Map(); + } + this.currentStickyLines = newMap; + break; + } + + let nextSectionEntry = undefined; + for (let j = 1; j < visibleRange.end - i; j++) { + // find next code cell after this one + const cellCheck = this.notebookEditor.cellAt(i + j); + if (cellCheck && cellCheck.cellKind === CellKind.Code) { + nextSectionEntry = this.getVisibleOutlineEntry(i + j); + break; + } + } + const nextSectionStickyHeight = this.computeStickyHeight(nextSectionEntry!); + + // this block of logic cleans transitions between two sections that share a parent. + // if the current section and the next section share a parent, then we can render the next section's sticky lines to avoid pop-in between + if (entry?.parent?.parent === nextSectionEntry?.parent) { + const linesToRender = Math.floor((sectionBottom - editorScrollTop) / 22) + 1; + let newMap: Map | undefined = new Map(); + newMap = this.renderStickyLines(nextSectionEntry?.parent, this.domNode, linesToRender, newMap); + if (!newMap) { + newMap = new Map(); + } + this.currentStickyLines = newMap; + break; + } else if (Math.abs(currentSectionStickyHeight - nextSectionStickyHeight) > 22) { // only shrink sticky + const linesToRender = Math.floor((sectionBottom - editorScrollTop) / 22); + let newMap: Map | undefined = new Map(); + newMap = this.renderStickyLines(entry?.parent, this.domNode, linesToRender, newMap); + if (!newMap) { + newMap = new Map(); + } + this.currentStickyLines = newMap; + break; + } + } + } else { + // there is no next cell, so use the bottom of the editor as the sectionBottom, using scrolltop + height + sectionBottom = this.notebookEditor.scrollTop + this.notebookEditor.getLayoutInfo().scrollHeight; + trackedEntry = this.getVisibleOutlineEntry(i); + const linesToRender = Math.floor((sectionBottom - editorScrollTop) / 22); + + let newMap: Map | undefined = new Map(); + newMap = this.renderStickyLines(trackedEntry?.parent, this.domNode, linesToRender, newMap); + if (!newMap) { + newMap = new Map(); + } + this.currentStickyLines = newMap; + + break; + } + } // cell loop close + this.updateDisplay(); + } + + private updateDisplay() { + const hasChildren = this.domNode.hasChildNodes(); + if (!hasChildren) { + this.domNode.style.display = 'none'; + } else { + this.domNode.style.display = 'block'; + } + this.setTop(); + } + + private computeStickyHeight(entry: OutlineEntry) { + let height = 0; + while (entry.parent) { + height += 22; + entry = entry.parent; + } + return height; + } + + private renderStickyLines(entry: OutlineEntry | undefined, containerElement: HTMLElement, numLinesToRender: number, newMap: Map) { + const partial = false; + let currentEntry = entry; + + const elementsToRender = []; + while (currentEntry) { + const lineToRender = this.createStickyElement(currentEntry, partial); + newMap.set(currentEntry, lineToRender); + elementsToRender.unshift(lineToRender); + currentEntry = currentEntry.parent; + } + + // iterate over elements to render, and append to container + // break when we reach numLinesToRender + for (let i = 0; i < elementsToRender.length; i++) { + if (i >= numLinesToRender) { + break; + } + containerElement.append(elementsToRender[i].element); + } + + containerElement.append(DOM.$('div', { class: 'notebook-shadow' })); // ensure we have dropShadow at base of sticky scroll + return newMap; + } + + private createStickyElement(entry: OutlineEntry, partial: boolean) { + const stickyElement = document.createElement('div'); + stickyElement.classList.add('notebook-sticky-scroll-line'); + stickyElement.innerText = '#'.repeat(entry.level) + ' ' + entry.label; + + // todo: partial line rendering for animater + if (partial) { + // const partialHeight = Math.floor(remainder * 22); + // stickyLine.style.height = `${partialHeight}px`; + } + + return new NotebookStickyLine(stickyElement, entry, this.notebookEditor); + } + + override dispose() { + this._disposables.dispose(); + super.dispose(); + } +} + +registerAction2(ToggleNotebookStickyScroll); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 473d46907b1d7..9a9ae9e0fdb57 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -12,7 +12,6 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { uppercaseFirstLetter } from 'vs/base/common/strings'; -import 'vs/css!./notebookKernelActionViewItem'; import { Command } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts index 7eb8d15c4d3a2..c530cacc2ebdd 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./notebookKernelActionViewItem'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action, IAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts index 498635a2fc67e..ce31818ac2798 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts @@ -46,7 +46,7 @@ export class NotebookOverviewRuler extends Themable { } private _render(ctx: CanvasRenderingContext2D, width: number, height: number, scrollHeight: number, ratio: number) { - const viewModel = this.notebookEditor._getViewModel(); + const viewModel = this.notebookEditor.getViewModel(); const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; const laneWidth = width / this._lanes; diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts index 74ed0aaf1583a..82d4fcd2a116b 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts @@ -118,5 +118,8 @@ export class NotebookCellOutputTextModel extends Disposable implements ICellOutp }; } + bumpVersion() { + this._versionId = this._versionId + 1; + } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d031650968a14..98d19932e6f7d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -218,6 +218,7 @@ export interface ICellOutput { replaceData(items: IOutputDto): void; appendData(items: IOutputItemDto[]): void; appendedSinceVersion(versionId: number, mime: string): VSBuffer | undefined; + bumpVersion(): void; } export interface CellInternalMetadataChangedEvent { @@ -927,6 +928,7 @@ export const NotebookSetting = { focusIndicator: 'notebook.cellFocusIndicator', insertToolbarLocation: 'notebook.insertToolbarLocation', globalToolbar: 'notebook.globalToolbar', + stickyScroll: 'notebook.stickyScroll.enabled', undoRedoPerCell: 'notebook.undoRedoPerCell', consolidatedOutputButton: 'notebook.consolidatedOutputButton', showFoldingControls: 'notebook.showFoldingControls', diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index afdd543fde967..323abe63dda52 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; -import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { IFileIconTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; @@ -17,7 +16,7 @@ import { CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib import { IActiveNotebookEditor, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; - +import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; suite('Notebook Outline', function () { diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts index b8f060ed210eb..0d8e843411ff0 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts @@ -5,25 +5,17 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; suite('NotebookCellList', () => { let disposables: DisposableStore; let instantiationService: TestInstantiationService; - let notebookDefaultOptions: NotebookOptions; - let topInsertToolbarHeight: number; suiteSetup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); - notebookDefaultOptions = new NotebookOptions(instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), false); - topInsertToolbarHeight = notebookDefaultOptions.computeTopInsertToolbarHeight(); - }); suiteTeardown(() => disposables.dispose()); @@ -51,7 +43,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + topInsertToolbarHeight, 100); + cellList.layout(210, 100); // scroll a bit, scrollTop to bottom: 5, 215 cellList.scrollTop = 5; @@ -98,7 +90,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + topInsertToolbarHeight, 100); + cellList.layout(210, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -144,7 +136,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + topInsertToolbarHeight, 100); + cellList.layout(210, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -179,7 +171,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + topInsertToolbarHeight, 100); + cellList.layout(210, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -221,7 +213,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + topInsertToolbarHeight, 100); + cellList.layout(210, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -274,7 +266,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + topInsertToolbarHeight, 100); + cellList.layout(210, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -310,7 +302,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + topInsertToolbarHeight, 100); + cellList.layout(210, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index fd3cf5fdcb975..2c3ffb72e73ae 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -221,7 +221,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic override notebookOptions = notebookOptions; override onDidChangeModel: Event = new Emitter().event; override onDidChangeCellState: Event = new Emitter().event; - override _getViewModel(): NotebookViewModel { + override getViewModel(): NotebookViewModel { return viewModel; } override textModel = viewModel.notebookDocument; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index eacb0db0e0583..ccac1bfdf60f1 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -492,6 +492,9 @@ .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown * { margin: 0px; } +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown *:not(:last-child) { + margin-bottom: 8px; +} .settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button { opacity: 0.9; diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 0d392743fcfc4..71973bd765387 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -187,46 +187,37 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } private registerSettingsActions() { - const registerOpenSettingsActionDisposables = this._register(new DisposableStore()); - const registerOpenSettingsAction = () => { - registerOpenSettingsActionDisposables.clear(); - const getTitle = (title: string) => !this.userDataProfileService.currentProfile.isDefault && this.userDataProfileService.currentProfile.useDefaultFlags?.settings - ? `${title} (${nls.localize('default profile', "Default Profile")})` - : title; - registerOpenSettingsActionDisposables.add(registerAction2(class extends Action2 { - constructor() { - super({ - id: SETTINGS_COMMAND_OPEN_SETTINGS, - title: { - value: getTitle(nls.localize('settings', "Settings")), - mnemonicTitle: getTitle(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings")), - original: 'Settings' - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - when: null, - primary: KeyMod.CtrlCmd | KeyCode.Comma, - }, - menu: [{ - id: MenuId.GlobalActivity, - group: '2_configuration', - order: 1 - }, { - id: MenuId.MenubarPreferencesMenu, - group: '2_configuration', - order: 1 - }], - }); - } - run(accessor: ServicesAccessor, args: string | IOpenSettingsActionOptions) { - // args takes a string for backcompat - const opts = typeof args === 'string' ? { query: args } : sanitizeOpenSettingsArgs(args); - return accessor.get(IPreferencesService).openSettings(opts); - } - })); - }; - registerOpenSettingsAction(); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => registerOpenSettingsAction())); + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_COMMAND_OPEN_SETTINGS, + title: { + value: nls.localize('settings', "Settings"), + mnemonicTitle: nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), + original: 'Settings' + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: null, + primary: KeyMod.CtrlCmd | KeyCode.Comma, + }, + menu: [{ + id: MenuId.GlobalActivity, + group: '2_configuration', + order: 1 + }, { + id: MenuId.MenubarPreferencesMenu, + group: '2_configuration', + order: 1 + }], + }); + } + run(accessor: ServicesAccessor, args: string | IOpenSettingsActionOptions) { + // args takes a string for backcompat + const opts = typeof args === 'string' ? { query: args } : sanitizeOpenSettingsArgs(args); + return accessor.get(IPreferencesService).openSettings(opts); + } + })); registerAction2(class extends Action2 { constructor() { super({ @@ -305,13 +296,13 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } }); - const registerOpenUserSettingsEditorFromJsonActionDisposable = this._register(new MutableDisposable()); + const registerOpenUserSettingsEditorFromJsonActionDisposables = this._register(new MutableDisposable()); const openUserSettingsEditorWhen = ContextKeyExpr.and( ContextKeyExpr.or(ResourceContextKey.Resource.isEqualTo(this.userDataProfileService.currentProfile.settingsResource.toString()), ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), ContextKeyExpr.not('isInDiffEditor')); const registerOpenUserSettingsEditorFromJsonAction = () => { - registerOpenUserSettingsEditorFromJsonActionDisposable.value = registerAction2(class extends Action2 { + registerOpenUserSettingsEditorFromJsonActionDisposables.value = registerAction2(class extends Action2 { constructor() { super({ id: '_workbench.openUserSettingsEditor', @@ -815,58 +806,49 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon private registerKeybindingsActions() { const that = this; const category = { value: nls.localize('preferences', "Preferences"), original: 'Preferences' }; - const registerOpenGlobalKeybindingsActionDisposables = this._register(new DisposableStore()); - const registerOpenGlobalKeybindingsAction = () => { - registerOpenGlobalKeybindingsActionDisposables.clear(); - const id = 'workbench.action.openGlobalKeybindings'; - const shortTitle = !that.userDataProfileService.currentProfile.isDefault && that.userDataProfileService.currentProfile.useDefaultFlags?.keybindings - ? nls.localize('keyboardShortcutsFromDefault', "Keyboard Shortcuts ({0})", nls.localize('default profile', "Default Profile")) - : nls.localize('keyboardShortcuts', "Keyboard Shortcuts"); - registerOpenGlobalKeybindingsActionDisposables.add(registerAction2(class extends Action2 { - constructor() { - super({ - id, - title: { value: nls.localize('openGlobalKeybindings', "Open Keyboard Shortcuts"), original: 'Open Keyboard Shortcuts' }, - shortTitle, - category, - icon: preferencesOpenSettingsIcon, - keybinding: { - when: null, - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyS) - }, - menu: [ - { id: MenuId.CommandPalette }, - { - id: MenuId.EditorTitle, - when: ResourceContextKey.Resource.isEqualTo(that.userDataProfileService.currentProfile.keybindingsResource.toString()), - group: 'navigation', - order: 1, - }, - { - id: MenuId.GlobalActivity, - group: '2_configuration', - order: 3 - } - ] - }); - } - run(accessor: ServicesAccessor, args: string | undefined) { - const query = typeof args === 'string' ? args : undefined; - return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query }); - } - })); - registerOpenGlobalKeybindingsActionDisposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - command: { + const id = 'workbench.action.openGlobalKeybindings'; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ id, - title: shortTitle, - }, - group: '2_configuration', - order: 3 - })); - }; - registerOpenGlobalKeybindingsAction(); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => registerOpenGlobalKeybindingsAction())); + title: { value: nls.localize('openGlobalKeybindings', "Open Keyboard Shortcuts"), original: 'Open Keyboard Shortcuts' }, + shortTitle: nls.localize('keyboardShortcuts', "Keyboard Shortcuts"), + category, + icon: preferencesOpenSettingsIcon, + keybinding: { + when: null, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyS) + }, + menu: [ + { id: MenuId.CommandPalette }, + { + id: MenuId.EditorTitle, + when: ResourceContextKey.Resource.isEqualTo(that.userDataProfileService.currentProfile.keybindingsResource.toString()), + group: 'navigation', + order: 1, + }, + { + id: MenuId.GlobalActivity, + group: '2_configuration', + order: 3 + } + ] + }); + } + run(accessor: ServicesAccessor, args: string | undefined) { + const query = typeof args === 'string' ? args : undefined; + return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query }); + } + })); + this._register(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + command: { + id, + title: nls.localize('keyboardShortcuts', "Keyboard Shortcuts"), + }, + group: '2_configuration', + order: 3 + })); registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index c80d1d641c896..19a69d1f1a0f4 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -355,11 +355,10 @@ class EditSettingRenderer extends Disposable { private onEditSettingClicked(editPreferenceWidget: EditPreferenceWidget, e: IEditorMouseEvent): void { EventHelper.stop(e.event, true); - const anchor = { x: e.event.posx, y: e.event.posy }; const actions = this.getSettings(editPreferenceWidget.getLine()).length === 1 ? this.getActions(editPreferenceWidget.preferences[0], this.getConfigurationsMap()[editPreferenceWidget.preferences[0].key]) : editPreferenceWidget.preferences.map(setting => new SubmenuAction(`preferences.submenu.${setting.key}`, setting.key, this.getActions(setting, this.getConfigurationsMap()[setting.key]))); this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => e.event, getActions: () => actions }); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index ff00480a0ee12..684dea2a3ef8c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -2526,8 +2526,11 @@ class ApplySettingToAllProfilesAction extends Action { } const newValue = distinct(value); - await this.configService.updateValue(APPLY_ALL_PROFILES_SETTING, newValue.length ? newValue : undefined, ConfigurationTarget.USER_LOCAL); - if (!this.checked) { + if (this.checked) { + await this.configService.updateValue(this.setting.key, this.configService.inspect(this.setting.key).userLocal?.value, ConfigurationTarget.USER_LOCAL); + await this.configService.updateValue(APPLY_ALL_PROFILES_SETTING, newValue.length ? newValue : undefined, ConfigurationTarget.USER_LOCAL); + } else { + await this.configService.updateValue(APPLY_ALL_PROFILES_SETTING, newValue.length ? newValue : undefined, ConfigurationTarget.USER_LOCAL); await this.configService.updateValue(this.setting.key, this.configService.inspect(this.setting.key).userLocal?.value, ConfigurationTarget.USER_LOCAL); } } diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 66ac320105c1d..433c796498cbf 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -76,7 +76,7 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu const viewEnabled: boolean = !!forwardedPortsViewEnabled.getValue(this.contextKeyService); - if (this.environmentService.remoteAuthority && viewEnabled) { + if (viewEnabled) { const viewContainer = await this.getViewContainer(); const tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService, this.tunnelService), this.environmentService); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); @@ -84,7 +84,7 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu this.remoteExplorerService.enablePortsFeatures(); viewsRegistry.registerViews([tunnelPanelDescriptor!], viewContainer); } - } else if (this.environmentService.remoteAuthority) { + } else { this.contextKeyListener = this.contextKeyService.onDidChangeContext(e => { if (e.affectsSome(new Set(forwardedPortsViewEnabled.keys()))) { this.enableForwardedPortsView(); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 1450fb17d1719..a5edf30c6cf22 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -725,6 +725,27 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } } + if (this.extensionGalleryService.isEnabled() && this.remoteMetadataInitialized) { + + const notInstalledItems: QuickPickItem[] = []; + for (const metadata of this.remoteExtensionMetadata) { + if (!metadata.installed && metadata.isPlatformCompatible) { + // Create Install QuickPick with a help link + const label = metadata.startConnectLabel; + const buttons: IQuickInputButton[] = [{ + iconClass: ThemeIcon.asClassName(infoIcon), + tooltip: nls.localize('remote.startActions.help', "Learn More") + }]; + notInstalledItems.push({ type: 'item', id: metadata.id, label: label, buttons: buttons }); + } + } + + items.push({ + type: 'separator', label: nls.localize('remote.startActions.install', 'Install') + }); + items.push(...notInstalledItems); + } + items.push({ type: 'separator' }); @@ -759,27 +780,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr items.pop(); // remove the separator again } - if (this.extensionGalleryService.isEnabled() && this.remoteMetadataInitialized) { - - const notInstalledItems: QuickPickItem[] = []; - for (const metadata of this.remoteExtensionMetadata) { - if (!metadata.installed && metadata.isPlatformCompatible) { - // Create Install QuickPick with a help link - const label = metadata.startConnectLabel; - const buttons: IQuickInputButton[] = [{ - iconClass: ThemeIcon.asClassName(infoIcon), - tooltip: nls.localize('remote.startActions.help', "Learn More") - }]; - notInstalledItems.push({ type: 'item', id: metadata.id, label: label, buttons: buttons }); - } - } - - items.push({ - type: 'separator', label: nls.localize('remote.startActions.install', 'Install') - }); - items.push(...notInstalledItems); - } - return items; }; diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 46c2d29079f3f..0c539b4a8aa08 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -625,6 +625,12 @@ export class FileMatch extends Disposable implements IFileMatch { setSelectedMatch(match: Match | null): void { if (match) { + + if (!this.isMatchSelected(match) && match instanceof MatchInNotebook) { + this._selectedMatch = match; + return; + } + if (!this._textMatches.has(match.id())) { return; } @@ -765,6 +771,9 @@ export class FileMatch extends Disposable implements IFileMatch { this._findMatchDecorationModel?.stopWebviewFind(); this._findMatchDecorationModel?.dispose(); this._findMatchDecorationModel = new FindMatchDecorationModel(this._notebookEditorWidget, this.searchInstanceID); + if (this._selectedMatch instanceof MatchInNotebook) { + this.highlightCurrentFindMatchDecoration(this._selectedMatch); + } } private _removeNotebookHighlights(): void { @@ -780,6 +789,7 @@ export class FileMatch extends Disposable implements IFileMatch { return; } + const oldCellMatches = new Map(this._cellMatches); if (this._notebookEditorWidget.getId() !== this._lastEditorWidgetIdForUpdate) { this._cellMatches.clear(); this._lastEditorWidgetIdForUpdate = this._notebookEditorWidget.getId(); @@ -790,10 +800,9 @@ export class FileMatch extends Disposable implements IFileMatch { let existingCell = this._cellMatches.get(match.cell.id); if (this._notebookEditorWidget && !existingCell) { const index = this._notebookEditorWidget.getCellIndex(match.cell); - const existingRawCell = this._cellMatches.get(`${rawCellPrefix}${index}`); + const existingRawCell = oldCellMatches.get(`${rawCellPrefix}${index}`); if (existingRawCell) { existingRawCell.setCellModel(match.cell); - this._cellMatches.delete(`${rawCellPrefix}${index}`); existingCell = existingRawCell; } } @@ -805,6 +814,9 @@ export class FileMatch extends Disposable implements IFileMatch { }); this._findMatchDecorationModel?.setAllFindMatchesDecorations(matches); + if (this._selectedMatch instanceof MatchInNotebook) { + this.highlightCurrentFindMatchDecoration(this._selectedMatch); + } this._onChange.fire({ forceUpdateModel: modelChange }); } diff --git a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts index 0b86f1c4fa2fe..9d2a199fe186e 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts @@ -4,20 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { isValidBasename } from 'vs/base/common/extpath'; -import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { extname } from 'vs/base/common/path'; import { basename, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; import * as nls from 'vs/nls'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SnippetsAction } from 'vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets'; import { SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; @@ -223,97 +221,80 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS await textFileService.write(pick.filepath, contents); } -export class ConfigureSnippetsActions extends Disposable implements IWorkbenchContribution { - - constructor( - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - ) { - super(); - const disposable = this._register(new MutableDisposable()); - disposable.value = this.registerAction(); - this._register(userDataProfileService.onDidChangeCurrentProfile(() => disposable.value = this.registerAction())); +export class ConfigureSnippetsAction extends SnippetsAction { + constructor() { + super({ + id: 'workbench.action.openSnippets', + title: { + value: nls.localize('openSnippet.label', "Configure User Snippets"), + original: 'Configure User Snippets' + }, + shortTitle: { + value: nls.localize('userSnippets', "User Snippets"), + mnemonicTitle: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), + original: 'User Snippets' + }, + f1: true, + menu: [ + { id: MenuId.MenubarPreferencesMenu, group: '2_configuration', order: 4 }, + { id: MenuId.GlobalActivity, group: '2_configuration', order: 4 }, + ] + }); } - private registerAction(): IDisposable { - const getTitle = (title: string) => !this.userDataProfileService.currentProfile.isDefault && this.userDataProfileService.currentProfile.useDefaultFlags?.snippets - ? `${title} (${nls.localize('default', "Default Profile")})` - : title; - return registerAction2(class extends SnippetsAction { - constructor() { - super({ - id: 'workbench.action.openSnippets', - title: { - value: nls.localize('openSnippet.label', "Configure User Snippets"), - original: 'Configure User Snippets' - }, - shortTitle: { - value: getTitle(nls.localize('userSnippets', "User Snippets")), - mnemonicTitle: getTitle(nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets")), - original: 'User Snippets' - }, - f1: true, - menu: [ - { id: MenuId.MenubarPreferencesMenu, group: '2_configuration', order: 4 }, - { id: MenuId.GlobalActivity, group: '2_configuration', order: 4 }, - ] - }); - } - - async run(accessor: ServicesAccessor): Promise { - - const snippetService = accessor.get(ISnippetsService); - const quickInputService = accessor.get(IQuickInputService); - const opener = accessor.get(IOpenerService); - const languageService = accessor.get(ILanguageService); - const userDataProfileService = accessor.get(IUserDataProfileService); - const workspaceService = accessor.get(IWorkspaceContextService); - const fileService = accessor.get(IFileService); - const textFileService = accessor.get(ITextFileService); - const labelService = accessor.get(ILabelService); - - const picks = await computePicks(snippetService, userDataProfileService, languageService, labelService); - const existing: QuickPickInput[] = picks.existing; - - type SnippetPick = IQuickPickItem & { uri: URI } & { scope: string }; - const globalSnippetPicks: SnippetPick[] = [{ - scope: nls.localize('new.global_scope', 'global'), - label: nls.localize('new.global', "New Global Snippets file..."), - uri: userDataProfileService.currentProfile.snippetsHome - }]; - - const workspaceSnippetPicks: SnippetPick[] = []; - for (const folder of workspaceService.getWorkspace().folders) { - workspaceSnippetPicks.push({ - scope: nls.localize('new.workspace_scope', "{0} workspace", folder.name), - label: nls.localize('new.folder', "New Snippets file for '{0}'...", folder.name), - uri: folder.toResource('.vscode') - }); - } + async run(accessor: ServicesAccessor): Promise { + + const snippetService = accessor.get(ISnippetsService); + const quickInputService = accessor.get(IQuickInputService); + const opener = accessor.get(IOpenerService); + const languageService = accessor.get(ILanguageService); + const userDataProfileService = accessor.get(IUserDataProfileService); + const workspaceService = accessor.get(IWorkspaceContextService); + const fileService = accessor.get(IFileService); + const textFileService = accessor.get(ITextFileService); + const labelService = accessor.get(ILabelService); + + const picks = await computePicks(snippetService, userDataProfileService, languageService, labelService); + const existing: QuickPickInput[] = picks.existing; + + type SnippetPick = IQuickPickItem & { uri: URI } & { scope: string }; + const globalSnippetPicks: SnippetPick[] = [{ + scope: nls.localize('new.global_scope', 'global'), + label: nls.localize('new.global', "New Global Snippets file..."), + uri: userDataProfileService.currentProfile.snippetsHome + }]; + + const workspaceSnippetPicks: SnippetPick[] = []; + for (const folder of workspaceService.getWorkspace().folders) { + workspaceSnippetPicks.push({ + scope: nls.localize('new.workspace_scope', "{0} workspace", folder.name), + label: nls.localize('new.folder', "New Snippets file for '{0}'...", folder.name), + uri: folder.toResource('.vscode') + }); + } - if (existing.length > 0) { - existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") }); - existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); - } else { - existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); - } + if (existing.length > 0) { + existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") }); + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); + } else { + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); + } - const pick = await quickInputService.pick(([] as QuickPickInput[]).concat(existing, globalSnippetPicks, workspaceSnippetPicks, picks.future), { - placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"), - matchOnDescription: true - }); - - if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); - } else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); - } else if (ISnippetPick.is(pick)) { - if (pick.hint) { - await createLanguageSnippetFile(pick, fileService, textFileService); - } - return opener.open(pick.filepath); - } + const pick = await quickInputService.pick(([] as QuickPickInput[]).concat(existing, globalSnippetPicks, workspaceSnippetPicks, picks.future), { + placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"), + matchOnDescription: true + }); + if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) { + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); + } else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) { + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); + } else if (ISnippetPick.is(pick)) { + if (pick.hint) { + await createLanguageSnippetFile(pick, fileService, textFileService); } - }); + return opener.open(pick.filepath); + } + } } diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index b78f735e1fcfb..0ad6445077f6d 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -11,7 +11,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { ConfigureSnippetsActions } from 'vs/workbench/contrib/snippets/browser/commands/configureSnippets'; +import { ConfigureSnippetsAction } from 'vs/workbench/contrib/snippets/browser/commands/configureSnippets'; import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; import { InsertSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/insertSnippet'; import { SurroundWithSnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet'; @@ -32,10 +32,10 @@ registerAction2(InsertSnippetAction); CommandsRegistry.registerCommandAlias('editor.action.showSnippets', 'editor.action.insertSnippet'); registerAction2(SurroundWithSnippetEditorAction); registerAction2(ApplyFileSnippetAction); +registerAction2(ConfigureSnippetsAction); // workbench contribs const workbenchContribRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchContribRegistry.registerWorkbenchContribution(ConfigureSnippetsActions, LifecyclePhase.Restored); workbenchContribRegistry.registerWorkbenchContribution(SnippetCodeActions, LifecyclePhase.Restored); // config diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index 39a65eac89638..ca743c05c81e3 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -244,6 +244,28 @@ const PyModulesToLookFor = [ 'playwright' ]; +const GoModulesToLookFor = [ + 'github.com/Azure/azure-sdk-for-go/sdk/storage/azblob', + 'github.com/Azure/azure-sdk-for-go/sdk/storage/azfile', + 'github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue', + 'github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel', + 'github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azadmin', + 'github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates', + 'github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys', + 'github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets', + 'github.com/Azure/azure-sdk-for-go/sdk/monitor/azquery', + 'github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs', + 'github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus', + 'github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig', + 'github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos', + 'github.com/Azure/azure-sdk-for-go/sdk/data/aztables', + 'github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry', + 'github.com/Azure/azure-sdk-for-go/sdk/cognitiveservices/azopenai', + 'github.com/Azure/azure-sdk-for-go/sdk/azidentity', + 'github.com/Azure/azure-sdk-for-go/sdk/azcore' +]; + + export class WorkspaceTagsService implements IWorkspaceTagsService { declare readonly _serviceBrand: undefined; private _tags: Tags | undefined; @@ -565,7 +587,25 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.azure-communication-administration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-security-attestation" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-data-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-data-tables" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + "workspace.py.azure-data-tables" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azadmin" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/monitor/azquery" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/data/aztables" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/cognitiveservices/azopenai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/azidentity" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/azcore" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ private async resolveWorkspaceTags(): Promise { @@ -624,6 +664,8 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { tags['workspace.py.app'] = nameSet.has('app.py'); tags['workspace.py.pyproject'] = nameSet.has('pyproject.toml'); + tags['workspace.go.mod'] = nameSet.has('go.mod'); + const mainActivity = nameSet.has('mainactivity.cs') || nameSet.has('mainactivity.fs'); const appDelegate = nameSet.has('appdelegate.cs') || nameSet.has('appdelegate.fs'); const androidManifest = nameSet.has('androidmanifest.xml'); @@ -752,6 +794,36 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { } }); + const goModPromises = getFilePromises('go.mod', this.fileService, this.textFileService, content => { + try { + const lines: string[] = splitLines(content.value); + let firstRequireBlockFound: boolean = false; + for (let i = 0; i < lines.length; i++) { + const line: string = lines[i].trim(); + if (line.startsWith('require (')) { + if (!firstRequireBlockFound) { + firstRequireBlockFound = true; + continue; + } else { + break; + } + } + if (line.startsWith(')')) { + break; + } + if (firstRequireBlockFound && line !== '') { + const packageName: string = line.split(' ')[0].trim(); + if (GoModulesToLookFor.indexOf(packageName) > -1) { + tags['workspace.go.mod.' + packageName] = true; + } + } + } + } + catch (e) { + // Ignore errors when resolving file or parsing file contents + } + }); + const pomPromises = getFilePromises('pom.xml', this.fileService, this.textFileService, content => { try { let dependenciesContent; @@ -792,7 +864,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { }); }); - return Promise.all([...packageJsonPromises, ...requirementsTxtPromises, ...pipfilePromises, ...pomPromises, ...gradlePromises, ...androidPromises]).then(() => tags); + return Promise.all([...packageJsonPromises, ...requirementsTxtPromises, ...pipfilePromises, ...pomPromises, ...gradlePromises, ...androidPromises, ...goModPromises]).then(() => tags); }); } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 5051478d34ac0..d20bb9d437083 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -39,7 +39,6 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; import { TerminalMenuBarGroup } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; import { isString } from 'vs/base/common/types'; -import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(RunAutomaticTasks, LifecyclePhase.Eventually); @@ -355,43 +354,32 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { class UserTasksGlobalActionContribution extends Disposable implements IWorkbenchContribution { - constructor( - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - ) { + constructor() { super(); this.registerActions(); } private registerActions() { - const registerOpenUserTasksActionDisposables = this._register(new DisposableStore()); - const registerOpenSettingsAction = () => { - registerOpenUserTasksActionDisposables.clear(); - const id = 'workbench.action.tasks.openUserTasks'; - const userTasksTitle = nls.localize('userTasks', "User Tasks"); - const title = !this.userDataProfileService.currentProfile.isDefault && this.userDataProfileService.currentProfile.useDefaultFlags?.tasks - ? `${userTasksTitle} (${nls.localize('default profile', "Default Profile")})` - : userTasksTitle; - registerOpenUserTasksActionDisposables.add(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - command: { - id, - title - }, - when: TaskExecutionSupportedContext, - group: '2_configuration', - order: 4 - })); - registerOpenUserTasksActionDisposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - command: { - id, - title - }, - when: TaskExecutionSupportedContext, - group: '2_configuration', - order: 4 - })); - }; - registerOpenSettingsAction(); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => registerOpenSettingsAction())); + const id = 'workbench.action.tasks.openUserTasks'; + const title = nls.localize('userTasks', "User Tasks"); + this._register(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + command: { + id, + title + }, + when: TaskExecutionSupportedContext, + group: '2_configuration', + order: 4 + })); + this._register(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + command: { + id, + title + }, + when: TaskExecutionSupportedContext, + group: '2_configuration', + order: 4 + })); } } workbenchRegistry.registerWorkbenchContribution(UserTasksGlobalActionContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index f57623f7ca81c..ec146c02cf6b1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -176,6 +176,7 @@ export interface ITerminalService extends ITerminalInstanceHost { readonly onDidChangeInstanceColor: Event<{ instance: ITerminalInstance; userInitiated: boolean }>; readonly onDidChangeInstancePrimaryStatus: Event; readonly onDidInputInstanceData: Event; + readonly onDidChangeSelection: Event; readonly onDidRegisterProcessSupport: Event; readonly onDidChangeConnectionState: Event; @@ -546,6 +547,7 @@ export interface ITerminalInstance { onDidRequestFocus: Event; onDidBlur: Event; onDidInputData: Event; + onDidChangeSelection: Event; /** * An event that fires when a terminal is dropped on this instance via drag and drop. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts index d847bcdff562f..d3ec1a63d6b32 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts @@ -12,7 +12,6 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView export function openContextMenu(event: MouseEvent, parent: HTMLElement, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: IAction[]): void { const standardEvent = new StandardMouseEvent(event); - const anchor: { x: number; y: number } = { x: standardEvent.posx, y: standardEvent.posy }; const actions: IAction[] = []; createAndFillInContextMenuActions(menu, undefined, actions); @@ -22,7 +21,7 @@ export function openContextMenu(event: MouseEvent, parent: HTMLElement, menu: IM } contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => standardEvent, getActions: () => actions, getActionsContext: () => parent, }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 91e2db9339b82..f814059bd6d54 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -316,6 +316,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onDidBlur = this._onDidBlur.event; private readonly _onDidInputData = this._register(new Emitter()); readonly onDidInputData = this._onDidInputData.event; + private readonly _onDidChangeSelection = this._register(new Emitter()); + readonly onDidChangeSelection = this._onDidChangeSelection.event; private readonly _onRequestAddInstanceToGroup = this._register(new Emitter()); readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event; private readonly _onDidChangeHasChildProcesses = this._register(new Emitter()); @@ -1722,6 +1724,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _onSelectionChange(): Promise { + this._onDidChangeSelection.fire(this); if (this._configurationService.getValue(TerminalSettingId.CopyOnSelection)) { if (this.hasSelection()) { await this.copySelection(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index b0e8a2c08cdc7..27ff2a40b233c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -152,6 +152,8 @@ export class TerminalService implements ITerminalService { get onDidChangeInstancePrimaryStatus(): Event { return this._onDidChangeInstancePrimaryStatus.event; } private readonly _onDidInputInstanceData = new Emitter(); get onDidInputInstanceData(): Event { return this._onDidInputInstanceData.event; } + private readonly _onDidChangeSelection = new Emitter(); + get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } private readonly _onDidDisposeGroup = new Emitter(); get onDidDisposeGroup(): Event { return this._onDidDisposeGroup.event; } private readonly _onDidChangeGroups = new Emitter(); @@ -836,7 +838,8 @@ export class TerminalService implements ITerminalService { instance.onMaximumDimensionsChanged(() => this._onDidMaxiumumDimensionsChange.fire(instance)), instance.onDidInputData(this._onDidInputInstanceData.fire, this._onDidInputInstanceData), instance.onDidFocus(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance), - instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e)) + instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e)), + instance.onDidChangeSelection(this._onDidChangeSelection.fire, this._onDidChangeSelection) ]; instance.onDisposed(() => dispose(instanceDisposables)); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index f91f800c2ad0b..1f70e8d088a19 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -47,6 +47,7 @@ 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 { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export class TerminalViewPane extends ViewPane { private _fontStyleElement: HTMLElement | undefined; @@ -77,7 +78,8 @@ export class TerminalViewPane extends ViewPane { @IMenuService private readonly _menuService: IMenuService, @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, - @IThemeService private readonly _themeService: IThemeService + @IThemeService private readonly _themeService: IThemeService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { super(options, keybindingService, _contextMenuService, _configurationService, _contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); this._register(this._terminalService.onDidRegisterProcessSupport(() => { @@ -245,7 +247,7 @@ export class TerminalViewPane extends ViewPane { if (action instanceof MenuItemAction) { const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu); this._newDropdown?.dispose(); - this._newDropdown = new DropdownWithPrimaryActionViewItem(action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}, this._keybindingService, this._notificationService, this._contextKeyService, this._themeService); + this._newDropdown = new DropdownWithPrimaryActionViewItem(action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}, this._keybindingService, this._notificationService, this._contextKeyService, this._themeService, this._accessibilityService); this._updateTabActionBar(this._terminalProfileService.availableProfiles); return this._newDropdown; } @@ -360,11 +362,12 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { @IContextMenuService contextMenuService: IContextMenuService, @ICommandService private readonly _commandService: ICommandService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IAccessibilityService _accessibilityService: IAccessibilityService ) { super(action, { draggable: true, hoverDelegate: _instantiationService.createInstance(SingleTabHoverDelegate) - }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService); + }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, _accessibilityService); // Register listeners to update the tab this._register(Event.debounce>(Event.any( diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index 9e364caf3da7e..2bd6fe3006404 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -111,7 +111,7 @@ registerTerminalAction({ precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: [ { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG, weight: KeybindingWeight.WorkbenchContrib + 2, when: TerminalContextKeys.accessibleBufferFocus } diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts index d5d73c308a1e2..a442d9933fa3e 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts @@ -10,6 +10,7 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ShellIntegrationStatus, WindowsShellType } from 'vs/platform/terminal/common/terminal'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -34,7 +35,7 @@ export class TerminalAccessibleContentProvider extends Disposable implements IAc ariaLabel: localize('terminal-help-label', "terminal accessibility help"), readMoreUrl: 'https://code.visualstudio.com/docs/editor/accessibility#_terminal-accessibility' }; - verbositySettingKey: string = 'terminal'; + verbositySettingKey = AccessibilityVerbositySettingId.Terminal; constructor( private readonly _instance: Pick, diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts index b28e397f7d195..da07be8e62cb3 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts @@ -13,15 +13,14 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; -import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import type { Terminal } from 'xterm'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; const enum ClassName { Active = 'active', @@ -62,7 +61,7 @@ export abstract class TerminalAccessibleWidget extends DisposableStore { this._element.classList.add(ClassName.Widget); this._editorContainer = document.createElement('div'); const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: [...EditorExtensionsRegistry.getEditorContributions(), ...EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID, 'editor.contrib.selectionAnchorController'])] + contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID) }; const font = _xterm.getFont(); const editorOptions: IEditorConstructionOptions = { 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 dbd35c08605fd..077dfd1d72366 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 @@ -6,6 +6,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -112,7 +113,7 @@ registerActiveInstanceAction({ keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, weight: KeybindingWeight.WorkbenchContrib + 1, - when: TerminalContextKeys.focus, + when: ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.accessibleBufferFocus) }, run: (activeInstance) => TerminalLinkContribution.get(activeInstance)?.showLinkQuickpick() }); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index d5121a326aa19..b7ca6213607ab 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -290,6 +290,7 @@ export class TestingExplorerView extends ViewPane { this.countSummary = text; this.renderActivityCount(); })); + this.testProgressService.update(); const listContainer = dom.append(this.container, dom.$('.test-explorer-tree')); this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 9d737a611845e..8bc667b3ebdb9 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -2174,7 +2174,7 @@ export class OpenMessageInEditorAction extends Action2 { id: OpenMessageInEditorAction.ID, f1: false, title: { value: localize('testing.openMessageInEditor', "Open in Editor"), original: 'Open in Editor' }, - icon: Codicon.linkExternal, + icon: Codicon.goToFile, category: Categories.Test, menu: [{ id: MenuId.TestPeekTitle }], }); diff --git a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts index 63a83e504dc63..4d9037d9b4387 100644 --- a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts @@ -115,7 +115,7 @@ export class TestingProgressUiService extends Disposable implements ITestingProg this.updateCountsEmitter.fire(collected); this.updateTextEmitter.fire(getTestProgressText(false, collected)); } else { - this.updateTextEmitter.fire(''); + this.updateTextEmitter.fire('\xA0'); this.updateCountsEmitter.fire(collectTestStateCounts(false)); } diff --git a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts index 6916d87256c88..ba09ebe897f81 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts @@ -219,7 +219,7 @@ class Snapper { public captureSyntaxTokens(fileName: string, content: string): Promise { const languageId = this.languageService.guessLanguageIdByFilepathOrFirstLine(URI.file(fileName)); - return this.textMateService.createGrammar(languageId!).then((grammar) => { + return this.textMateService.createTokenizer(languageId!).then((grammar) => { if (!grammar) { return []; } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfileCreateWidget.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfileCreateWidget.css deleted file mode 100644 index 899cf1a074a4c..0000000000000 --- a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfileCreateWidget.css +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.profile-type-widget { - display: flex; - margin: 0px 6px 8px 11px; - align-items: center; - justify-content: space-between; - font-size: 12px; -} - -.profile-type-widget>.profile-type-select-container { - overflow: hidden; - padding-left: 10px; - flex: 1; - display: flex; - align-items: center; - justify-content: center; -} - -.profile-type-widget>.profile-type-select-container>.monaco-select-box { - cursor: pointer; - line-height: 17px; - padding: 2px 23px 2px 8px; - border-radius: 2px; -} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 429f95af11e32..4ff1c808b9e4a 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -3,19 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/userDataProfileCreateWidget'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; -import { Event } from 'vs/base/common/event'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; import { Action2, IMenuService, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IUserDataProfile, IUserDataProfilesService, ProfileResourceType, UseDefaultProfileFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, IS_CURRENT_PROFILE_TRANSIENT_CONTEXT, IS_PROFILE_IMPORT_IN_PROGRESS_CONTEXT, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, PROFILES_CATEGORY, PROFILE_FILTER, IS_PROFILE_EXPORT_IN_PROGRESS_CONTEXT, ProfilesMenu, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TITLE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, IS_CURRENT_PROFILE_TRANSIENT_CONTEXT, IS_PROFILE_IMPORT_IN_PROGRESS_CONTEXT, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, PROFILES_CATEGORY, PROFILE_FILTER, IS_PROFILE_EXPORT_IN_PROGRESS_CONTEXT, ProfilesMenu, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TITLE, IProfileTemplateInfo } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; @@ -25,22 +23,8 @@ import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspac import { getErrorMessage } from 'vs/base/common/errors'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IRequestService, asJson } from 'vs/platform/request/common/request'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { ILogService } from 'vs/platform/log/common/log'; -import Severity from 'vs/base/common/severity'; -import { $, append } from 'vs/base/browser/dom'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { isString } from 'vs/base/common/types'; - -interface IProfileTemplateInfo { - readonly name: string; - readonly url: string; -} + +type IProfileTemplateQuickPickItem = IQuickPickItem & IProfileTemplateInfo; export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { @@ -57,14 +41,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, @IContextKeyService contextKeyService: IContextKeyService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @INotificationService private readonly notificationService: INotificationService, @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IProductService private readonly productService: IProductService, - @IRequestService private readonly requestService: IRequestService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextViewService private readonly contextViewService: IContextViewService, - @ILogService private readonly logService: ILogService, ) { super(); @@ -215,6 +192,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements value: localize('edit profile', "Edit Profile..."), original: `Edit Profile...` }, + f1: true, menu: [ { id: ProfilesMenu, @@ -226,7 +204,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }); } run() { - return that.saveProfile(that.userDataProfileService.currentProfile); + return that.userDataProfileImportExportService.editProfile(that.userDataProfileService.currentProfile); } }); } @@ -308,6 +286,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements private registerImportProfileAction(): IDisposable { const disposables = new DisposableStore(); const id = 'workbench.profiles.actions.importProfile'; + const that = this; disposables.add(registerAction2(class ImportProfileAction extends Action2 { constructor() { super({ @@ -340,21 +319,41 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements const disposables = new DisposableStore(); const quickPick = disposables.add(quickInputService.createQuickPick()); + const profileTemplateQuickPickItems = await that.getProfileTemplatesQuickPickItems(); + const updateQuickPickItems = (value?: string) => { - const selectFromFileItem: IQuickPickItem = { label: localize('import from file', "Create from profile template file") }; - quickPick.items = value ? [{ label: localize('import from url', "Create from profile template URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; + const quickPickItems: (IQuickPickItem | IQuickPickSeparator)[] = []; + if (value) { + quickPickItems.push({ label: quickPick.value, description: localize('import from url', "Import from URL") }); + } + quickPickItems.push({ label: localize('import from file', "Select File...") }); + if (profileTemplateQuickPickItems.length) { + quickPickItems.push({ + type: 'separator', + label: localize('templates', "Profile Templates") + }, ...profileTemplateQuickPickItems); + } + quickPick.items = quickPickItems; }; - quickPick.title = localize('import profile quick pick title', "Create Profile from Profile Template..."); - quickPick.placeholder = localize('import profile placeholder', "Provide profile template URL or select profile template file"); + + quickPick.title = localize('import profile quick pick title', "Import from Profile Template..."); + quickPick.placeholder = localize('import profile placeholder', "Provide Profile Template URL"); quickPick.ignoreFocusOut = true; disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); updateQuickPickItems(); quickPick.matchOnLabel = false; quickPick.matchOnDescription = false; disposables.add(quickPick.onDidAccept(async () => { + quickPick.hide(); + const selectedItem = quickPick.selectedItems[0]; + if (!selectedItem) { + return; + } try { - quickPick.hide(); - const profile = quickPick.selectedItems[0].description ? URI.parse(quickPick.value) : await this.getProfileUriFromFileSystem(fileDialogService); + if ((selectedItem).url) { + return await that.userDataProfileImportExportService.createProfile(URI.parse((selectedItem).url)); + } + const profile = selectedItem.label === quickPick.value ? URI.parse(quickPick.value) : await this.getProfileUriFromFileSystem(fileDialogService); if (profile) { await userDataProfileImportExportService.importProfile(profile); } @@ -410,194 +409,11 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } run(accessor: ServicesAccessor) { - return that.saveProfile(undefined, that.userDataProfileService.currentProfile); + return that.userDataProfileImportExportService.createProfile(that.userDataProfileService.currentProfile); } })); } - private async saveProfile(profile: IUserDataProfile): Promise; - private async saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | string): Promise; - private async saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | string): Promise { - - type CreateProfileInfoClassification = { - owner: 'sandy081'; - comment: 'Report when profile is about to be created'; - }; - this.telemetryService.publicLog2<{}, CreateProfileInfoClassification>('userDataProfile.startCreate'); - - const disposables = new DisposableStore(); - const title = profile ? localize('save profile', "Edit {0} Profile...", profile.name) : localize('create new profle', "Create New Profile..."); - - const settings: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Settings, label: localize('settings', "Settings"), picked: !profile?.useDefaultFlags?.settings }; - const keybindings: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Keybindings, label: localize('keybindings', "Keyboard Shortcuts"), picked: !profile?.useDefaultFlags?.keybindings }; - const snippets: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Snippets, label: localize('snippets', "User Snippets"), picked: !profile?.useDefaultFlags?.snippets }; - const tasks: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Tasks, label: localize('tasks', "User Tasks"), picked: !profile?.useDefaultFlags?.tasks }; - const extensions: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Extensions, label: localize('extensions', "Extensions"), picked: !profile?.useDefaultFlags?.extensions }; - const resources = [settings, keybindings, snippets, tasks, extensions]; - - const quickPick = this.quickInputService.createQuickPick(); - quickPick.title = title; - quickPick.placeholder = localize('name placeholder', "Profile name"); - quickPick.value = profile?.name ?? ''; - quickPick.canSelectMany = true; - quickPick.matchOnDescription = false; - quickPick.matchOnDetail = false; - quickPick.matchOnLabel = false; - quickPick.sortByLabel = false; - quickPick.hideCountBadge = true; - quickPick.ok = false; - quickPick.customButton = true; - quickPick.hideCheckAll = true; - quickPick.ignoreFocusOut = true; - quickPick.customLabel = profile ? localize('save', "Save") : localize('create', "Create"); - quickPick.description = localize('customise the profile', "Choose what to configure in your Profile:"); - quickPick.items = [...resources]; - - const update = () => { - quickPick.items = resources; - quickPick.selectedItems = resources.filter(item => item.picked); - }; - update(); - - const validate = () => { - if (!profile && this.userDataProfilesService.profiles.some(p => p.name === quickPick.value)) { - quickPick.validationMessage = localize('profileExists', "Profile with name {0} already exists.", quickPick.value); - quickPick.severity = Severity.Warning; - return; - } - if (resources.every(resource => !resource.picked)) { - quickPick.validationMessage = localize('invalid configurations', "The profile should contain at least one configuration."); - quickPick.severity = Severity.Warning; - return; - } - quickPick.severity = Severity.Ignore; - quickPick.validationMessage = undefined; - }; - - disposables.add(quickPick.onDidChangeSelection(items => { - let needUpdate = false; - for (const resource of resources) { - resource.picked = items.includes(resource); - const description = resource.picked ? undefined : localize('use default profile', "Using Default Profile"); - if (resource.description !== description) { - resource.description = description; - needUpdate = true; - } - } - if (needUpdate) { - update(); - } - validate(); - })); - - disposables.add(quickPick.onDidChangeValue(validate)); - - let result: { name: string; items: ReadonlyArray } | undefined; - disposables.add(Event.any(quickPick.onDidCustom, quickPick.onDidAccept)(() => { - if (!quickPick.value) { - quickPick.validationMessage = localize('name required', "Provide a name for the new profile"); - quickPick.severity = Severity.Error; - } - if (quickPick.validationMessage) { - return; - } - result = { name: quickPick.value, items: quickPick.selectedItems }; - quickPick.hide(); - quickPick.severity = Severity.Ignore; - quickPick.validationMessage = undefined; - })); - - if (!profile) { - const domNode = $('.profile-type-widget'); - append(domNode, $('.profile-type-create-label', undefined, localize('create from', "Copy from:"))); - const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; - const profileOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | string })[] = []; - profileOptions.push({ text: localize('empty profile', "None") }); - const templates = await this.getProfileTemplatesFromProduct(); - if (templates.length) { - profileOptions.push({ ...separator, decoratorRight: localize('from templates', "Profile Templates") }); - for (const template of templates) { - profileOptions.push({ text: template.name, id: template.url, source: template.url }); - } - } - profileOptions.push({ ...separator, decoratorRight: localize('from existing profiles', "Existing Profiles") }); - for (const profile of this.userDataProfilesService.profiles) { - profileOptions.push({ text: profile.name, id: profile.id, source: profile }); - } - - const findOptionIndex = () => { - const index = profileOptions.findIndex(option => { - if (isString(source)) { - return option.id === source; - } else if (source) { - return option.id === source.id; - } - return false; - }); - return index > -1 ? index : 0; - }; - - const selectBox = disposables.add(this.instantiationService.createInstance(SelectBox, profileOptions, findOptionIndex(), this.contextViewService, defaultSelectBoxStyles, { useCustomDrawn: true })); - selectBox.render(append(domNode, $('.profile-type-select-container'))); - quickPick.widget = domNode; - - const updateOptions = () => { - const option = profileOptions[findOptionIndex()]; - for (const resource of resources) { - resource.picked = option.source && !isString(option.source) ? !option.source?.useDefaultFlags?.[resource.id] : true; - } - update(); - }; - - updateOptions(); - disposables.add(selectBox.onDidSelect(({ index }) => { - source = profileOptions[index].source; - updateOptions(); - })); - } - - quickPick.show(); - - await new Promise((c, e) => { - disposables.add(quickPick.onDidHide(() => { - disposables.dispose(); - c(); - })); - }); - - if (!result) { - this.telemetryService.publicLog2<{}, CreateProfileInfoClassification>('userDataProfile.cancelCreate'); - return; - } - - this.telemetryService.publicLog2<{}, CreateProfileInfoClassification>('userDataProfile.successCreate'); - - try { - const useDefaultFlags: UseDefaultProfileFlags | undefined = result.items.length === resources.length - ? undefined - : { - settings: !result.items.includes(settings), - keybindings: !result.items.includes(keybindings), - snippets: !result.items.includes(snippets), - tasks: !result.items.includes(tasks), - extensions: !result.items.includes(extensions) - }; - if (profile) { - await this.userDataProfileManagementService.updateProfile(profile, { name: result.name, useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); - } else { - if (isString(source)) { - await this.userDataProfileImportExportService.importProfile(URI.parse(source), { mode: 'apply', name: result.name, useDefaultFlags }); - } else if (source) { - await this.userDataProfileImportExportService.createFromProfile(source, result.name, { useDefaultFlags }); - } else { - await this.userDataProfileManagementService.createAndEnterProfile(result.name, { useDefaultFlags }); - } - } - } catch (error) { - this.notificationService.error(error); - } - } - private registerCreateProfileAction(): void { const that = this; this._register(registerAction2(class CreateProfileAction extends Action2 { @@ -623,7 +439,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { - return that.saveProfile(); + return that.userDataProfileImportExportService.createProfile(); } })); } @@ -701,20 +517,16 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements })); } - private async getProfileTemplatesFromProduct(): Promise { - if (this.productService.profileTemplatesUrl) { - try { - const context = await this.requestService.request({ type: 'GET', url: this.productService.profileTemplatesUrl }, CancellationToken.None); - if (context.res.statusCode === 200) { - return (await asJson(context)) || []; - } else { - this.logService.error('Could not get profile templates.', context.res.statusCode); - } - } catch (error) { - this.logService.error(error); - } + private async getProfileTemplatesQuickPickItems(): Promise { + const quickPickItems: IProfileTemplateQuickPickItem[] = []; + const profileTemplates = await this.userDataProfileManagementService.getBuiltinProfileTemplates(); + for (const template of profileTemplates) { + quickPickItems.push({ + label: template.name, + ...template + }); } - return []; + return quickPickItems; } private async reportWorkspaceProfileInfo(): Promise { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilePreview.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilePreview.ts index 095948fa26f55..4dd3d92a76173 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilePreview.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilePreview.ts @@ -20,7 +20,7 @@ export class UserDataProfilePreviewContribution extends Disposable implements IW ) { super(); if (environmentService.options?.profileToPreview) { - userDataProfileImportExportService.importProfile(URI.revive(environmentService.options.profileToPreview), { mode: 'preview' }) + userDataProfileImportExportService.importProfile(URI.revive(environmentService.options.profileToPreview), { mode: 'both' }) .then(null, error => logService.error('Error while previewing the profile', getErrorMessage(error))); } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 9c5207bfcfc1e..db98aa06c59ea 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -627,10 +627,6 @@ display: inline; } -.monaco-workbench .part.editor>.content .gettingStartedContainer .index-list.getting-started div { - text-align: center; -} - .monaco-workbench .part.editor>.content .gettingStartedContainer.noExtensions .index-list.featured-extensions { display: none; } diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 108a13bdfb1f7..ec3b40e33cb55 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -229,6 +229,12 @@ import { applicationConfigurationNodeBase } from 'vs/workbench/common/configurat 'description': localize('windowControlsOverlay', "Use window controls provided by the platform instead of our HTML-based window controls. Changes require a full restart to apply."), 'included': isWindows }, + 'window.experimental.nativeContextMenuLocation': { // TODO@bpasero remove me eventually + 'type': 'boolean', + 'default': false, + 'scope': ConfigurationScope.APPLICATION, + 'description': localize('nativeContextMenuLocation', "Let the OS handle positioning of the context menu in cases where it should appear under the mouse.") + }, 'window.dialogStyle': { 'type': 'string', 'enum': ['native', 'custom'], diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 9609e5f96bb7d..d2a26882677c2 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -5,10 +5,10 @@ import { localize } from 'vs/nls'; import product from 'vs/platform/product/common/product'; -import { INativeWindowConfiguration, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { INativeWindowConfiguration, IWindowsConfiguration } from 'vs/platform/window/common/window'; import { Workbench } from 'vs/workbench/browser/workbench'; import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; -import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser'; +import { setFullscreen } from 'vs/base/browser/browser'; import { domContentLoaded } from 'vs/base/browser/dom'; import { onUnexpectedError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; @@ -58,6 +58,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; import { RemoteSocketFactoryService, IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; import { ElectronRemoteResourceLoader } from 'vs/platform/remote/electron-sandbox/electronRemoteResourceLoader'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; export class DesktopMain extends Disposable { @@ -74,10 +76,7 @@ export class DesktopMain extends Disposable { // Massage configuration file URIs this.reviveUris(); - // Browser config - const zoomLevel = this.configuration.zoomLevel || 0; - setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); - setZoomLevel(zoomLevel, true /* isTrusted */); + // Apply fullscreen early if configured setFullscreen(!!this.configuration.fullscreen); } @@ -112,6 +111,13 @@ export class DesktopMain extends Disposable { // Init services and wait for DOM to be ready in parallel const [services] = await Promise.all([this.initServices(), domContentLoaded()]); + // Apply zoom level early once we have a configuration service + // and before the workbench is created to prevent flickering. + // We also need to respect that zoom level can be configured per + // workspace, so we need the resolved configuration service. + // (fixes https://github.com/microsoft/vscode/issues/187982) + this.applyConfiguredWindowZoomLevel(services.configurationService); + // Create Workbench const workbench = new Workbench(document.body, { extraClasses: this.getExtraClasses() }, services.serviceCollection, services.logService); @@ -125,6 +131,13 @@ export class DesktopMain extends Disposable { this._register(instantiationService.createInstance(NativeWindow)); } + private applyConfiguredWindowZoomLevel(configurationService: IConfigurationService) { + const windowConfig = configurationService.getValue(); + const windowZoomLevel = typeof windowConfig.window?.zoomLevel === 'number' ? windowConfig.window.zoomLevel : 0; + + applyZoom(windowZoomLevel); + } + private getExtraClasses(): string[] { if (isMacintosh) { if (this.configuration.os.release > '20.0.0') { @@ -142,7 +155,7 @@ export class DesktopMain extends Disposable { this._register(workbench.onDidShutdown(() => this.dispose())); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection; logService: ILogService; storageService: NativeWorkbenchStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection; logService: ILogService; storageService: NativeWorkbenchStorageService; configurationService: IConfigurationService }> { const serviceCollection = new ServiceCollection(); @@ -310,7 +323,7 @@ export class DesktopMain extends Disposable { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - return { serviceCollection, logService, storageService }; + return { serviceCollection, logService, storageService, configurationService }; } private resolveWorkspaceIdentifier(environmentService: INativeWorkbenchEnvironmentService): IAnyWorkspaceIdentifier { diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 12859500a5882..7ab81debcbe34 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -79,8 +79,6 @@ export class NativeWindow extends Disposable { private readonly customTitleContextMenuDisposable = this._register(new DisposableStore()); - private previousConfiguredZoomLevel: number | undefined; - private readonly addFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddFolders(), 100)); private pendingFoldersToAdd: URI[] = []; @@ -320,7 +318,6 @@ export class NativeWindow extends Disposable { }); // Zoom level changes - this.updateWindowZoomLevel(); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('window.zoomLevel')) { this.updateWindowZoomLevel(); @@ -605,21 +602,10 @@ export class NativeWindow extends Disposable { private updateWindowZoomLevel(): void { const windowConfig = this.configurationService.getValue(); + const windowZoomLevel = typeof windowConfig.window?.zoomLevel === 'number' ? windowConfig.window.zoomLevel : 0; - let configuredZoomLevel = 0; - if (windowConfig.window && typeof windowConfig.window.zoomLevel === 'number') { - configuredZoomLevel = windowConfig.window.zoomLevel; - - // Leave early if the configured zoom level did not change (https://github.com/microsoft/vscode/issues/1536) - if (this.previousConfiguredZoomLevel === configuredZoomLevel) { - return; - } - - this.previousConfiguredZoomLevel = configuredZoomLevel; - } - - if (getZoomLevel() !== configuredZoomLevel) { - applyZoom(configuredZoomLevel); + if (getZoomLevel() !== windowZoomLevel) { + applyZoom(windowZoomLevel); } } diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index 073f4766e9f51..66b9fb03b1901 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -23,7 +23,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { stripIcons } from 'vs/base/common/iconLabels'; import { coalesce } from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; -import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview'; +import { AnchorAlignment, AnchorAxisAlignment, isAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -54,7 +54,7 @@ export class ContextMenuService implements IContextMenuService { // Native context menu: otherwise else { - this.impl = new NativeContextMenuService(notificationService, telemetryService, keybindingService, menuService, contextKeyService); + this.impl = new NativeContextMenuService(notificationService, telemetryService, keybindingService, menuService, contextKeyService, configurationService); } } @@ -77,14 +77,28 @@ class NativeContextMenuService extends Disposable implements IContextMenuService private readonly _onDidHideContextMenu = this._store.add(new Emitter()); readonly onDidHideContextMenu = this._onDidHideContextMenu.event; + private useNativeContextMenuLocation = false; + constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); + + this.updateUseNativeContextMenuLocation(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('window.experimental.nativeContextMenuLocation')) { + this.updateUseNativeContextMenuLocation(); + } + })); + } + + private updateUseNativeContextMenuLocation(): void { + this.useNativeContextMenuLocation = this.configurationService.getValue('window.experimental.nativeContextMenuLocation') === true; } showContextMenu(delegate: IContextMenuDelegate | IContextMenuMenuDelegate): void { @@ -103,8 +117,8 @@ class NativeContextMenuService extends Disposable implements IContextMenuService const menu = this.createMenu(delegate, actions, onHide); const anchor = delegate.getAnchor(); - let x: number; - let y: number; + let x: number | undefined; + let y: number | undefined; let zoom = getZoomFactor(); if (dom.isHTMLElement(anchor)) { @@ -155,20 +169,28 @@ class NativeContextMenuService extends Disposable implements IContextMenuService if (isMacintosh) { y += 4 / zoom; } + } else if (isAnchor(anchor)) { + x = anchor.x; + y = anchor.y; } else { - const pos: { x: number; y: number } = anchor; - x = pos.x + 1; /* prevent first item from being selected automatically under mouse */ - y = pos.y; + if (this.useNativeContextMenuLocation) { + // We leave x/y undefined in this case which will result in + // Electron taking care of opening the menu at the cursor position. + } else { + x = anchor.posx + 1; // prevent first item from being selected automatically under mouse + y = anchor.posy; + } } - x *= zoom; - y *= zoom; + if (typeof x === 'number') { + x = Math.floor(x * zoom); + } + + if (typeof y === 'number') { + y = Math.floor(y * zoom); + } - popup(menu, { - x: Math.floor(x), - y: Math.floor(y), - positioningItem: delegate.autoSelectFirstItem ? 0 : undefined, - }, () => onHide()); + popup(menu, { x, y, positioningItem: delegate.autoSelectFirstItem ? 0 : undefined, }, () => onHide()); this._onDidShowContextMenu.fire(); } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index c251c3a399d09..347eeaefd5169 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -85,13 +85,16 @@ export const allApiProposals = Object.freeze({ terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', + terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', tokenInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', treeViewActiveItem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', + treeViewMarkdownMessage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts', treeViewReveal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.d.ts', + tunnelFactory: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts', tunnels: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts', windowActivity: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.windowActivity.d.ts', workspaceTrust: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts' diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index c9ae3c52101fd..911ad41e1ed40 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -521,6 +521,16 @@ export class BrowserHostService extends Disposable implements IHostService { window.close(); } + async withExpectedShutdown(expectedShutdownTask: () => Promise): Promise { + const previousShutdownReason = this.shutdownReason; + try { + this.shutdownReason = HostShutdownReason.Api; + return await expectedShutdownTask(); + } finally { + this.shutdownReason = previousShutdownReason; + } + } + private async handleExpectedShutdown(reason: ShutdownReason): Promise { // Update shutdown reason in a way that we do diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 06e5f15ef00e4..6072cff2843d7 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -88,5 +88,11 @@ export interface IHostService { */ close(): Promise; + /** + * Execute an asynchronous `expectedShutdownTask`. While this task is + * in progress, attempts to quit the application will not be vetoed with a dialog. + */ + withExpectedShutdown(expectedShutdownTask: () => Promise): Promise; + //#endregion } diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 72bb003e22f69..131972fa5385e 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -135,6 +135,10 @@ class WorkbenchHostService extends Disposable implements IHostService { return this.nativeHostService.closeWindow(); } + async withExpectedShutdown(expectedShutdownTask: () => Promise): Promise { + return await expectedShutdownTask(); + } + //#endregion } diff --git a/src/vs/workbench/services/issue/browser/issueTroubleshoot.ts b/src/vs/workbench/services/issue/browser/issueTroubleshoot.ts index 2d2e398a91f68..fe0e5a9f37ed5 100644 --- a/src/vs/workbench/services/issue/browser/issueTroubleshoot.ts +++ b/src/vs/workbench/services/issue/browser/issueTroubleshoot.ts @@ -161,6 +161,11 @@ class TroubleshootIssueService extends Disposable implements ITroubleshootIssueS } private async reproduceIssueWithExtensionsDisabled(): Promise { + if (!(await this.extensionManagementService.getInstalled(ExtensionType.User)).length) { + this.state = new TroubleShootState(TroubleshootStage.WORKBENCH, this.state!.profile); + return; + } + const result = await this.askToReproduceIssue(localize('profile.extensions.disabled', "Issue troubleshooting is active and has temporarily disabled all installed extensions. Check if you can still reproduce the problem and proceed by selecting from these options.")); if (result === 'good') { const profile = this.userDataProfilesService.profiles.find(p => p.id === this.state!.profile) ?? this.userDataProfilesService.defaultProfile; @@ -199,11 +204,11 @@ class TroubleshootIssueService extends Disposable implements ITroubleshootIssueS private askToReproduceIssue(message: string): Promise { return new Promise((c, e) => { const goodPrompt: IPromptChoice = { - label: localize('I cannot reproduce', "I can't reproduce"), + label: localize('I cannot reproduce', "I Can't Reproduce"), run: () => c('good') }; const badPrompt: IPromptChoice = { - label: localize('This is Bad', "I can reproduce"), + label: localize('This is Bad', "I Can Reproduce"), run: () => c('bad') }; const stop: IPromptChoice = { diff --git a/src/vs/workbench/services/localization/browser/localeService.ts b/src/vs/workbench/services/localization/browser/localeService.ts index 60ae11d8cc95d..37a957772616b 100644 --- a/src/vs/workbench/services/localization/browser/localeService.ts +++ b/src/vs/workbench/services/localization/browser/localeService.ts @@ -28,7 +28,7 @@ export class WebLocaleService implements ILocaleService { async setLocale(languagePackItem: ILanguagePackItem, _skipDialog = false): Promise { const locale = languagePackItem.id; - if (locale === Language.value() || (!locale && Language.value() === navigator.language)) { + if (locale === Language.value() || (!locale && Language.value() === navigator.language.toLowerCase())) { return; } if (locale) { @@ -57,7 +57,7 @@ export class WebLocaleService implements ILocaleService { window.localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_LOCALE_KEY); window.localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY); - if (Language.value() === navigator.language) { + if (Language.value() === navigator.language.toLowerCase()) { return; } diff --git a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts index 44d57e2549302..d89abd9110dd2 100644 --- a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts +++ b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts @@ -8,7 +8,7 @@ import { isLinux } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IEncryptionService, KnownStorageProvider, isGnome, isKwallet } from 'vs/platform/encryption/common/encryptionService'; +import { IEncryptionService, KnownStorageProvider, PasswordStoreCLIOption, isGnome, isKwallet } from 'vs/platform/encryption/common/encryptionService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; @@ -72,7 +72,7 @@ export class NativeSecretStorageService extends BaseSecretStorageService { label: localize('usePlainText', "Use weaker encryption"), run: async () => { await this._encryptionService.setUsePlainTextEncryption(); - await this._jsonEditingService.write(this._environmentService.argvResource, [{ path: ['password-store'], value: 'basic_text' }], true); + await this._jsonEditingService.write(this._environmentService.argvResource, [{ path: ['password-store'], value: PasswordStoreCLIOption.basic }], true); this.reinitialize(); } }; diff --git a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts similarity index 87% rename from src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController.ts rename to src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 72fed613e4136..8c0d074883b01 100644 --- a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -16,12 +16,14 @@ import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; -import { TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/worker/textMate.worker'; -import type { StateDeltas } from 'vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost'; +import type { StateDeltas, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; import type { applyStateStackDiff, StateStack } from 'vscode-textmate'; export class TextMateWorkerTokenizerController extends Disposable { - private _pendingChanges: IModelContentChangedEvent[] = []; + private static _id = 0; + + public readonly controllerId = TextMateWorkerTokenizerController._id++; + private readonly _pendingChanges: IModelContentChangedEvent[] = []; /** * These states will eventually equal the worker states. @@ -47,13 +49,13 @@ export class TextMateWorkerTokenizerController extends Disposable { this._register(keepAlive(this._loggingEnabled)); this._register(this._model.onDidChangeContent((e) => { - if (this.shouldLog) { + if (this._shouldLog) { console.log('model change', { fileName: this._model.uri.fsPath.split('\\').pop(), changes: changesToString(e.changes), }); } - this._worker.acceptModelChanged(this._model.uri.toString(), e); + this._worker.acceptModelChanged(this.controllerId, e); this._pendingChanges.push(e); })); @@ -62,7 +64,7 @@ export class TextMateWorkerTokenizerController extends Disposable { const encodedLanguageId = this._languageIdCodec.encodeLanguageId(languageId); this._worker.acceptModelLanguageChanged( - this._model.uri.toString(), + this.controllerId, languageId, encodedLanguageId ); @@ -78,27 +80,33 @@ export class TextMateWorkerTokenizerController extends Disposable { languageId, encodedLanguageId, maxTokenizationLineLength: this._maxTokenizationLineLength.get(), + controllerId: this.controllerId, }); this._register(autorun('update maxTokenizationLineLength', reader => { const maxTokenizationLineLength = this._maxTokenizationLineLength.read(reader); - this._worker.acceptMaxTokenizationLineLength(this._model.uri.toString(), maxTokenizationLineLength); + this._worker.acceptMaxTokenizationLineLength(this.controllerId, maxTokenizationLineLength); })); } - get shouldLog() { - return this._loggingEnabled.get(); - } - public override dispose(): void { super.dispose(); - this._worker.acceptRemovedModel(this._model.uri.toString()); + this._worker.acceptRemovedModel(this.controllerId); + } + + public requestTokens(startLineNumber: number, endLineNumberExclusive: number): void { + this._worker.retokenize(this.controllerId, startLineNumber, endLineNumberExclusive); } /** * This method is called from the worker through the worker host. */ - public async setTokensAndStates(versionId: number, rawTokens: ArrayBuffer, stateDeltas: StateDeltas[]): Promise { + public async setTokensAndStates(controllerId: number, versionId: number, rawTokens: ArrayBuffer, stateDeltas: StateDeltas[]): Promise { + if (this.controllerId !== controllerId) { + // This event is for an outdated controller (the worker didn't receive the delete/create messages yet), ignore the event. + return; + } + // _states state, change{k}, ..., change{versionId}, state delta base & rawTokens, change{j}, ..., change{m}, current renderer state // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ // | past changes | future states @@ -107,7 +115,7 @@ export class TextMateWorkerTokenizerController extends Disposable { new Uint8Array(rawTokens) ); - if (this.shouldLog) { + if (this._shouldLog) { console.log('received background tokenization result', { fileName: this._model.uri.fsPath.split('\\').pop(), updatedTokenLines: tokens.map((t) => t.getLineRange()).join(' & '), @@ -115,7 +123,7 @@ export class TextMateWorkerTokenizerController extends Disposable { }); } - if (this.shouldLog) { + if (this._shouldLog) { const changes = this._pendingChanges.filter(c => c.versionId <= versionId).map(c => c.changes).map(c => changesToString(c)).join(' then '); console.log('Applying changes to local states', changes); } @@ -130,7 +138,7 @@ export class TextMateWorkerTokenizerController extends Disposable { } if (this._pendingChanges.length > 0) { - if (this.shouldLog) { + if (this._shouldLog) { const changes = this._pendingChanges.map(c => c.changes).map(c => changesToString(c)).join(' then '); console.log('Considering non-processed changes', changes); } @@ -205,6 +213,9 @@ export class TextMateWorkerTokenizerController extends Disposable { // First set states, then tokens, so that events fired from set tokens don't read invalid states this._backgroundTokenizationStore.setTokens(tokens); } + + private get _shouldLog() { return this._loggingEnabled.get(); } + } function fullLineArrayEditFromModelContentChange(c: IModelContentChange[]): ArrayEdit { diff --git a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/threadedBackgroundTokenizerFactory.ts similarity index 68% rename from src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost.ts rename to src/vs/workbench/services/textMate/browser/backgroundTokenization/threadedBackgroundTokenizerFactory.ts index 723152d3d6b7e..d3a89b5a1781b 100644 --- a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/threadedBackgroundTokenizerFactory.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BugIndicatingError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { AppResourcePath, FileAccess, nodeModulesAsarPath, nodeModulesPath } from 'vs/base/common/network'; import { IObservable } from 'vs/base/common/observable'; @@ -20,18 +19,18 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IExtensionResourceLoaderService } from 'vs/platform/extensionResourceLoader/common/extensionResourceLoader'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICreateData, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/worker/textMate.worker'; -import { TextMateWorkerTokenizerController } from 'vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController'; +import { ICreateData, ITextMateWorkerHost, StateDeltas, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; +import { TextMateWorkerTokenizerController } from 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController'; import { IValidGrammarDefinition } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; -import type { IRawTheme, StackDiff } from 'vscode-textmate'; +import type { IRawTheme } from 'vscode-textmate'; -export class TextMateWorkerHost implements IDisposable { +export class ThreadedBackgroundTokenizerFactory implements IDisposable { private static _reportedMismatchingTokens = false; private _workerProxyPromise: Promise | null = null; private _worker: MonacoWebWorker | null = null; private _workerProxy: TextMateTokenizationWorker | null = null; - private readonly _workerTokenizerControllers = new Map(); + private readonly _workerTokenizerControllers = new Map(); private _currentTheme: IRawTheme | null = null; private _currentTokenColorMap: string[] | null = null; @@ -50,13 +49,64 @@ export class TextMateWorkerHost implements IDisposable { ) { } - public setGrammarDefinitions(grammarDefinitions: IValidGrammarDefinition[]): void { - this._grammarDefinitions = grammarDefinitions; - this._killWorker(); + public dispose(): void { + this._disposeWorker(); } - dispose(): void { - this._killWorker(); + // Will be recreated after worker is disposed (because tokenizer is re-registered when languages change) + public createBackgroundTokenizer(textModel: ITextModel, tokenStore: IBackgroundTokenizationStore, maxTokenizationLineLength: IObservable): IBackgroundTokenizer | undefined { + const shouldTokenizeAsync = this._configurationService.getValue('editor.experimental.asyncTokenization'); + // fallback to default sync background tokenizer + if (shouldTokenizeAsync !== true || textModel.isTooLargeForSyncing()) { return undefined; } + + const store = new DisposableStore(); + const controllerContainer = this._getWorkerProxy().then((workerProxy) => { + if (store.isDisposed || !workerProxy) { return undefined; } + + const controllerContainer = { controller: undefined as undefined | TextMateWorkerTokenizerController }; + store.add(keepAliveWhenAttached(textModel, () => { + const controller = new TextMateWorkerTokenizerController(textModel, workerProxy, this._languageService.languageIdCodec, tokenStore, this._configurationService, maxTokenizationLineLength); + controllerContainer.controller = controller; + this._workerTokenizerControllers.set(controller.controllerId, controller); + return toDisposable(() => { + controllerContainer.controller = undefined; + this._workerTokenizerControllers.delete(controller.controllerId); + controller.dispose(); + }); + })); + return controllerContainer; + }); + + return { + dispose() { + store.dispose(); + }, + requestTokens: async (startLineNumber, endLineNumberExclusive) => { + const controller = (await controllerContainer)?.controller; + if (controller) { + // If there is no controller, the model has been detached in the meantime + controller.requestTokens(startLineNumber, endLineNumberExclusive); + } + }, + reportMismatchingTokens: (lineNumber) => { + if (ThreadedBackgroundTokenizerFactory._reportedMismatchingTokens) { + return; + } + ThreadedBackgroundTokenizerFactory._reportedMismatchingTokens = true; + + this._notificationService.error({ + message: 'Async Tokenization Token Mismatch in line ' + lineNumber, + name: 'Async Tokenization Token Mismatch', + }); + + this._telemetryService.publicLog2<{}, { owner: 'hediet'; comment: 'Used to see if async tokenization is bug-free' }>('asyncTokenizationMismatchingTokens', {}); + }, + }; + } + + public setGrammarDefinitions(grammarDefinitions: IValidGrammarDefinition[]): void { + this._grammarDefinitions = grammarDefinitions; + this._disposeWorker(); } public acceptTheme(theme: IRawTheme, colorMap: string[]): void { @@ -67,14 +117,14 @@ export class TextMateWorkerHost implements IDisposable { } } - private getWorkerProxy(): Promise { + private _getWorkerProxy(): Promise { if (!this._workerProxyPromise) { - this._workerProxyPromise = this.createWorkerProxy(); + this._workerProxyPromise = this._createWorkerProxy(); } return this._workerProxyPromise; } - private async createWorkerProxy(): Promise { + private async _createWorkerProxy(): Promise { const textmateModuleLocation: AppResourcePath = `${nodeModulesPath}/vscode-textmate`; const textmateModuleLocationAsar: AppResourcePath = `${nodeModulesAsarPath}/vscode-textmate`; const onigurumaModuleLocation: AppResourcePath = `${nodeModulesPath}/vscode-oniguruma`; @@ -94,14 +144,30 @@ export class TextMateWorkerHost implements IDisposable { onigurumaMainUri: FileAccess.asBrowserUri(onigurumaMain).toString(true), onigurumaWASMUri: FileAccess.asBrowserUri(onigurumaWASM).toString(true), }; - const worker = createWebWorker(this._modelService, this._languageConfigurationService, { + const host: ITextMateWorkerHost = { + readFile: async (_resource: UriComponents): Promise => { + const resource = URI.revive(_resource); + return this._extensionResourceLoaderService.readExtensionResource(resource); + }, + setTokensAndStates: async (controllerId: number, versionId: number, tokens: Uint8Array, lineEndStateDeltas: StateDeltas[]): Promise => { + const controller = this._workerTokenizerControllers.get(controllerId); + // When a model detaches, it is removed synchronously from the map. + // However, the worker might still be sending tokens for that model, + // so we ignore the event when there is no controller. + if (controller) { + controller.setTokensAndStates(controllerId, versionId, tokens, lineEndStateDeltas); + } + }, + reportTokenizationTime: (timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, isRandomSample: boolean): void => { + this._reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, isRandomSample); + } + }; + const worker = this._worker = createWebWorker(this._modelService, this._languageConfigurationService, { createData, label: 'textMateWorker', - moduleId: 'vs/workbench/services/textMate/browser/worker/textMate.worker', - host: this, + moduleId: 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker', + host, }); - - this._worker = worker; const proxy = await worker.getProxy(); if (this._worker !== worker) { @@ -115,7 +181,7 @@ export class TextMateWorkerHost implements IDisposable { return proxy; } - private _killWorker(): void { + private _disposeWorker(): void { for (const controller of this._workerTokenizerControllers.values()) { controller.dispose(); } @@ -128,93 +194,6 @@ export class TextMateWorkerHost implements IDisposable { this._workerProxy = null; this._workerProxyPromise = null; } - - // Will be recreated when worker is killed (because tokenizer is re-registered when languages change) - public createBackgroundTokenizer(textModel: ITextModel, tokenStore: IBackgroundTokenizationStore, maxTokenizationLineLength: IObservable): IBackgroundTokenizer | undefined { - if (this._workerTokenizerControllers.has(textModel.uri.toString())) { - throw new BugIndicatingError(); - } - - const shouldTokenizeAsync = this._configurationService.getValue('editor.experimental.asyncTokenization'); - if (shouldTokenizeAsync !== true) { - return undefined; - } - - if (textModel.isTooLargeForSyncing()) { - // fallback to default sync background tokenizer - return undefined; - } - - const store = new DisposableStore(); - this.getWorkerProxy().then((workerProxy) => { - if (store.isDisposed || !workerProxy) { - return; - } - - store.add(keepAliveWhenAttached(textModel, () => { - const controller = new TextMateWorkerTokenizerController(textModel, workerProxy, this._languageService.languageIdCodec, tokenStore, this._configurationService, maxTokenizationLineLength); - this._workerTokenizerControllers.set(textModel.uri.toString(), controller); - - return toDisposable(() => { - this._workerTokenizerControllers.delete(textModel.uri.toString()); - controller.dispose(); - }); - })); - }); - - return { - dispose() { - store.dispose(); - }, - requestTokens: (startLineNumber, endLineNumberExclusive) => { - this.getWorkerProxy().then((workerProxy) => { - workerProxy?.retokenize(textModel.uri.toString(), startLineNumber, endLineNumberExclusive); - }); - }, - reportMismatchingTokens: (lineNumber) => { - if (TextMateWorkerHost._reportedMismatchingTokens) { - return; - } - TextMateWorkerHost._reportedMismatchingTokens = true; - - this._notificationService.error({ - message: 'Async Tokenization Token Mismatch in line ' + lineNumber, - name: 'Async Tokenization Token Mismatch', - }); - - this._telemetryService.publicLog2<{}, { owner: 'hediet'; comment: 'Used to see if async tokenization is bug-free' }>('asyncTokenizationMismatchingTokens', {}); - }, - }; - } - - // #region called by the worker - - async readFile(_resource: UriComponents): Promise { - const resource = URI.revive(_resource); - return this._extensionResourceLoaderService.readExtensionResource(resource); - } - - async setTokensAndStates(_resource: UriComponents, versionId: number, tokens: Uint8Array, lineEndStateDeltas: StateDeltas[]): Promise { - const resource = URI.revive(_resource); - const controller = this._workerTokenizerControllers.get(resource.toString()); - if (controller) { - // When a model detaches, it is removed synchronously from the map. - // However, the worker might still be sending tokens for that model. - controller.setTokensAndStates(versionId, tokens, lineEndStateDeltas); - } - } - - public reportTokenizationTime(timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, isRandomSample: boolean): void { - this._reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, isRandomSample); - } - - // #endregion -} - -export interface StateDeltas { - startLineNumber: number; - // null means the state for that line did not change - stateDeltas: (StackDiff | null)[]; } function keepAliveWhenAttached(textModel: ITextModel, factory: () => IDisposable): IDisposable { diff --git a/src/vs/workbench/services/textMate/browser/worker/textMate.worker.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker.ts similarity index 50% rename from src/vs/workbench/services/textMate/browser/worker/textMate.worker.ts rename to src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker.ts index 275db92323383..3974d642732a9 100644 --- a/src/vs/workbench/services/textMate/browser/worker/textMate.worker.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker.ts @@ -7,11 +7,23 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { IWorkerContext } from 'vs/editor/common/services/editorSimpleWorker'; -import type { StateDeltas, TextMateWorkerHost } from 'vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost'; import { ICreateGrammarResult, TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { IValidEmbeddedLanguagesMap, IValidGrammarDefinition, IValidTokenTypeMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; -import type { IOnigLib, IRawTheme } from 'vscode-textmate'; -import { TextMateWorkerModel } from './textMateWorkerModel'; +import type { IOnigLib, IRawTheme, StackDiff } from 'vscode-textmate'; +import { TextMateWorkerTokenizer } from './textMateWorkerTokenizer'; + +/** + * Defines the worker entry point. Must be exported and named `create`. + */ +export function create(ctx: IWorkerContext, createData: ICreateData): TextMateTokenizationWorker { + return new TextMateTokenizationWorker(ctx, createData); +} + +export interface ITextMateWorkerHost { + readFile(_resource: UriComponents): Promise; + setTokensAndStates(controllerId: number, versionId: number, tokens: Uint8Array, lineEndStateDeltas: StateDeltas[]): Promise; + reportTokenizationTime(timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, isRandomSample: boolean): void; +} export interface ICreateData { grammarDefinitions: IValidGrammarDefinitionDTO[]; @@ -32,16 +44,24 @@ export interface IValidGrammarDefinitionDTO { sourceExtensionId?: string; } -export class TextMateTokenizationWorker { +export interface StateDeltas { + startLineNumber: number; + // null means the state for that line did not change + stateDeltas: (StackDiff | null)[]; +} - private readonly _host: TextMateWorkerHost; - private readonly _models: { [uri: string]: TextMateWorkerModel } = Object.create(null); +export class TextMateTokenizationWorker { + private readonly _host: ITextMateWorkerHost; + private readonly _models = new Map(); private readonly _grammarCache: Promise[] = []; private readonly _grammarFactory: Promise; - constructor(ctx: IWorkerContext, private readonly createData: ICreateData) { + constructor( + ctx: IWorkerContext, + private readonly _createData: ICreateData + ) { this._host = ctx.host; - const grammarDefinitions = createData.grammarDefinitions.map((def) => { + const grammarDefinitions = _createData.grammarDefinitions.map((def) => { return { location: URI.revive(def.location), language: def.language, @@ -58,10 +78,10 @@ export class TextMateTokenizationWorker { } private async _loadTMGrammarFactory(grammarDefinitions: IValidGrammarDefinition[]): Promise { - const uri = this.createData.textmateMainUri; + const uri = this._createData.textmateMainUri; const vscodeTextmate = await import(uri); - const vscodeOniguruma = await import(this.createData.onigurumaMainUri); - const response = await fetch(this.createData.onigurumaWASMUri); + const vscodeOniguruma = await import(this._createData.onigurumaMainUri); + const response = await fetch(this._createData.onigurumaWASMUri); // Using the response directly only works if the server sets the MIME type 'application/wasm'. // Otherwise, a TypeError is thrown when using the streaming compiler. @@ -81,30 +101,48 @@ export class TextMateTokenizationWorker { }, grammarDefinitions, vscodeTextmate, onigLib); } - // #region called by renderer + // These methods are called by the renderer public acceptNewModel(data: IRawModelData): void { const uri = URI.revive(data.uri); - const key = uri.toString(); - this._models[key] = new TextMateWorkerModel(uri, data.lines, data.EOL, data.versionId, this, data.languageId, data.encodedLanguageId, data.maxTokenizationLineLength); + const that = this; + this._models.set(data.controllerId, new TextMateWorkerTokenizer(uri, data.lines, data.EOL, data.versionId, { + async getOrCreateGrammar(languageId: string, encodedLanguageId: LanguageId): Promise { + const grammarFactory = await that._grammarFactory; + if (!grammarFactory) { + return Promise.resolve(null); + } + if (!that._grammarCache[encodedLanguageId]) { + that._grammarCache[encodedLanguageId] = grammarFactory.createGrammar(languageId, encodedLanguageId); + } + return that._grammarCache[encodedLanguageId]; + }, + setTokensAndStates(versionId: number, tokens: Uint8Array, stateDeltas: StateDeltas[]): void { + that._host.setTokensAndStates(data.controllerId, versionId, tokens, stateDeltas); + }, + reportTokenizationTime(timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, isRandomSample: boolean): void { + that._host.reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, isRandomSample); + }, + }, data.languageId, data.encodedLanguageId, data.maxTokenizationLineLength)); } - public acceptModelChanged(strURL: string, e: IModelChangedEvent): void { - this._models[strURL].onEvents(e); + public acceptModelChanged(controllerId: number, e: IModelChangedEvent): void { + this._models.get(controllerId)!.onEvents(e); } - public retokenize(strURL: string, startLineNumber: number, endLineNumberExclusive: number): void { - this._models[strURL].retokenize(startLineNumber, endLineNumberExclusive); + public retokenize(controllerId: number, startLineNumber: number, endLineNumberExclusive: number): void { + this._models.get(controllerId)!.retokenize(startLineNumber, endLineNumberExclusive); } - public acceptModelLanguageChanged(strURL: string, newLanguageId: string, newEncodedLanguageId: LanguageId): void { - this._models[strURL].onLanguageId(newLanguageId, newEncodedLanguageId); + public acceptModelLanguageChanged(controllerId: number, newLanguageId: string, newEncodedLanguageId: LanguageId): void { + this._models.get(controllerId)!.onLanguageId(newLanguageId, newEncodedLanguageId); } - public acceptRemovedModel(strURL: string): void { - if (this._models[strURL]) { - this._models[strURL].dispose(); - delete this._models[strURL]; + public acceptRemovedModel(controllerId: number): void { + const model = this._models.get(controllerId); + if (model) { + model.dispose(); + this._models.delete(controllerId); } } @@ -113,34 +151,9 @@ export class TextMateTokenizationWorker { grammarFactory?.setTheme(theme, colorMap); } - public acceptMaxTokenizationLineLength(strURL: string, value: number): void { - this._models[strURL].acceptMaxTokenizationLineLength(value); + public acceptMaxTokenizationLineLength(controllerId: number, value: number): void { + this._models.get(controllerId)!.acceptMaxTokenizationLineLength(value); } - - // #endregion - - // #region called by worker model - - public async getOrCreateGrammar(languageId: string, encodedLanguageId: LanguageId): Promise { - const grammarFactory = await this._grammarFactory; - if (!grammarFactory) { - return Promise.resolve(null); - } - if (!this._grammarCache[encodedLanguageId]) { - this._grammarCache[encodedLanguageId] = grammarFactory.createGrammar(languageId, encodedLanguageId); - } - return this._grammarCache[encodedLanguageId]; - } - - public setTokensAndStates(resource: URI, versionId: number, tokens: Uint8Array, stateDeltas: StateDeltas[]): void { - this._host.setTokensAndStates(resource, versionId, tokens, stateDeltas); - } - - public reportTokenizationTime(timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, isRandomSample: boolean): void { - this._host.reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, isRandomSample); - } - - // #endregion } export interface IRawModelData { @@ -151,8 +164,5 @@ export interface IRawModelData { languageId: string; encodedLanguageId: LanguageId; maxTokenizationLineLength: number; -} - -export function create(ctx: IWorkerContext, createData: ICreateData): TextMateTokenizationWorker { - return new TextMateTokenizationWorker(ctx, createData); + controllerId: number; } diff --git a/src/vs/workbench/services/textMate/browser/worker/textMateWorkerModel.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts similarity index 59% rename from src/vs/workbench/services/textMate/browser/worker/textMateWorkerModel.ts rename to src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts index c39793848673e..a7c586b2442fe 100644 --- a/src/vs/workbench/services/textMate/browser/worker/textMateWorkerModel.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts @@ -16,25 +16,29 @@ import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contig import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; import { TextMateTokenizationSupport } from 'vs/workbench/services/textMate/browser/tokenizationSupport/textMateTokenizationSupport'; import { TokenizationSupportWithLineLimit } from 'vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit'; -import { StateDeltas } from 'vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost'; import type { StackDiff, StateStack, diffStateStacksRefEq } from 'vscode-textmate'; -import { TextMateTokenizationWorker } from './textMate.worker'; +import { ICreateGrammarResult } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; +import { StateDeltas } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; -export class TextMateWorkerModel extends MirrorTextModel { - private _tokenizationStateStore: TokenizerWithStateStore | null = null; +export interface TextMateModelTokenizerHost { + getOrCreateGrammar(languageId: string, encodedLanguageId: LanguageId): Promise; + setTokensAndStates(versionId: number, tokens: Uint8Array, stateDeltas: StateDeltas[]): void; + reportTokenizationTime(timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, isRandomSample: boolean): void; +} + +export class TextMateWorkerTokenizer extends MirrorTextModel { + private _tokenizerWithStateStore: TokenizerWithStateStore | null = null; private _isDisposed: boolean = false; - private readonly _maxTokenizationLineLength = observableValue( - '_maxTokenizationLineLength', - -1 - ); + private readonly _maxTokenizationLineLength = observableValue('_maxTokenizationLineLength', -1); private _diffStateStacksRefEqFn?: typeof diffStateStacksRefEq; + private readonly _tokenizeDebouncer = new RunOnceScheduler(() => this._tokenize(), 10); constructor( uri: URI, lines: string[], eol: string, versionId: number, - private readonly _worker: TextMateTokenizationWorker, + private readonly _host: TextMateModelTokenizerHost, private _languageId: string, private _encodedLanguageId: LanguageId, maxTokenizationLineLength: number, @@ -55,18 +59,11 @@ export class TextMateWorkerModel extends MirrorTextModel { this._resetTokenization(); } - private readonly tokenizeDebouncer = new RunOnceScheduler( - () => this._tokenize(), - 10 - ); - override onEvents(e: IModelChangedEvent): void { super.onEvents(e); - if (this._tokenizationStateStore) { - this._tokenizationStateStore.store.acceptChanges(e.changes); - } - this.tokenizeDebouncer.schedule(); + this._tokenizerWithStateStore?.store.acceptChanges(e.changes); + this._tokenizeDebouncer.schedule(); } public acceptMaxTokenizationLineLength(maxTokenizationLineLength: number): void { @@ -74,48 +71,44 @@ export class TextMateWorkerModel extends MirrorTextModel { } public retokenize(startLineNumber: number, endLineNumberExclusive: number) { - if (this._tokenizationStateStore) { - this._tokenizationStateStore.store.invalidateEndStateRange(new LineRange(startLineNumber, endLineNumberExclusive)); - this.tokenizeDebouncer.schedule(); + if (this._tokenizerWithStateStore) { + this._tokenizerWithStateStore.store.invalidateEndStateRange(new LineRange(startLineNumber, endLineNumberExclusive)); + this._tokenizeDebouncer.schedule(); } } - private _resetTokenization(): void { - this._tokenizationStateStore = null; + private async _resetTokenization() { + this._tokenizerWithStateStore = null; const languageId = this._languageId; const encodedLanguageId = this._encodedLanguageId; - this._worker.getOrCreateGrammar(languageId, encodedLanguageId).then((r) => { - if ( - this._isDisposed || - languageId !== this._languageId || - encodedLanguageId !== this._encodedLanguageId || - !r - ) { - return; - } - if (r.grammar) { - const tokenizationSupport = new TokenizationSupportWithLineLimit( - this._encodedLanguageId, - new TextMateTokenizationSupport(r.grammar, r.initialState, false, undefined, () => false, - (timeMs, lineLength, isRandomSample) => { - this._worker.reportTokenizationTime(timeMs, languageId, r.sourceExtensionId, lineLength, isRandomSample); - }, - false - ), - this._maxTokenizationLineLength - ); - this._tokenizationStateStore = new TokenizerWithStateStore(this._lines.length, tokenizationSupport); - } else { - this._tokenizationStateStore = null; - } - this._tokenize(); - }); + const r = await this._host.getOrCreateGrammar(languageId, encodedLanguageId); + + if (this._isDisposed || languageId !== this._languageId || encodedLanguageId !== this._encodedLanguageId || !r) { + return; + } + + if (r.grammar) { + const tokenizationSupport = new TokenizationSupportWithLineLimit( + this._encodedLanguageId, + new TextMateTokenizationSupport(r.grammar, r.initialState, false, undefined, () => false, + (timeMs, lineLength, isRandomSample) => { + this._host.reportTokenizationTime(timeMs, languageId, r.sourceExtensionId, lineLength, isRandomSample); + }, + false + ), + this._maxTokenizationLineLength + ); + this._tokenizerWithStateStore = new TokenizerWithStateStore(this._lines.length, tokenizationSupport); + } else { + this._tokenizerWithStateStore = null; + } + this._tokenize(); } private async _tokenize(): Promise { - if (this._isDisposed || !this._tokenizationStateStore) { + if (this._isDisposed || !this._tokenizerWithStateStore) { return; } @@ -132,25 +125,24 @@ export class TextMateWorkerModel extends MirrorTextModel { const stateDeltaBuilder = new StateDeltaBuilder(); while (true) { - const lineNumberToTokenize = this._tokenizationStateStore.store.getFirstInvalidEndStateLineNumber(); - if (lineNumberToTokenize === null || tokenizedLines > 200) { + const lineToTokenize = this._tokenizerWithStateStore.getFirstInvalidLine(); + if (lineToTokenize === null || tokenizedLines > 200) { break; } tokenizedLines++; - const text = this._lines[lineNumberToTokenize - 1]; - const lineStartState = this._tokenizationStateStore.getStartState(lineNumberToTokenize)!; - const r = this._tokenizationStateStore.tokenizationSupport.tokenizeEncoded(text, true, lineStartState); - if (this._tokenizationStateStore.store.setEndState(lineNumberToTokenize, r.endState as StateStack)) { - const delta = this._diffStateStacksRefEqFn(lineStartState, r.endState as StateStack); - stateDeltaBuilder.setState(lineNumberToTokenize, delta); + const text = this._lines[lineToTokenize.lineNumber - 1]; + const r = this._tokenizerWithStateStore.tokenizationSupport.tokenizeEncoded(text, true, lineToTokenize.startState); + if (this._tokenizerWithStateStore.store.setEndState(lineToTokenize.lineNumber, r.endState as StateStack)) { + const delta = this._diffStateStacksRefEqFn(lineToTokenize.startState, r.endState as StateStack); + stateDeltaBuilder.setState(lineToTokenize.lineNumber, delta); } else { - stateDeltaBuilder.setState(lineNumberToTokenize, null); + stateDeltaBuilder.setState(lineToTokenize.lineNumber, null); } LineTokens.convertToEndOffset(r.tokens, text.length); - tokenBuilder.add(lineNumberToTokenize, r.tokens); + tokenBuilder.add(lineToTokenize.lineNumber, r.tokens); const deltaMs = new Date().getTime() - startTime; if (deltaMs > 20) { @@ -164,8 +156,7 @@ export class TextMateWorkerModel extends MirrorTextModel { } const stateDeltas = stateDeltaBuilder.getStateDeltas(); - this._worker.setTokensAndStates( - this._uri, + this._host.setTokensAndStates( this._versionId, tokenBuilder.serialize(), stateDeltas diff --git a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeature.ts b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeature.ts index 4b23b5693591e..34cc1cc5eadf4 100644 --- a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeature.ts +++ b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeature.ts @@ -11,7 +11,7 @@ export const ITextMateTokenizationService = createDecorator; + createTokenizer(languageId: string): Promise; startDebugMode(printFn: (str: string) => void, onStop: () => void): void; } diff --git a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts index dd19178ffe930..19e7a72bdcb96 100644 --- a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts +++ b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts @@ -32,7 +32,7 @@ import { ExtensionMessageCollector, IExtensionPointUser } from 'vs/workbench/ser import { ITextMateTokenizationService } from 'vs/workbench/services/textMate/browser/textMateTokenizationFeature'; import { TextMateTokenizationSupport } from 'vs/workbench/services/textMate/browser/tokenizationSupport/textMateTokenizationSupport'; import { TokenizationSupportWithLineLimit } from 'vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit'; -import { TextMateWorkerHost } from 'vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost'; +import { ThreadedBackgroundTokenizerFactory } from 'vs/workbench/services/textMate/browser/backgroundTokenization/threadedBackgroundTokenizerFactory'; import { TMGrammarFactory, missingTMGrammarErrorMessage } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { ITMSyntaxExtensionPoint, grammarsExtPoint } from 'vs/workbench/services/textMate/common/TMGrammars'; import { IValidEmbeddedLanguagesMap, IValidGrammarDefinition, IValidTokenTypeMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; @@ -55,9 +55,9 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate private readonly _tokenizersRegistrations = new DisposableStore(); private _currentTheme: IRawTheme | null = null; private _currentTokenColorMap: string[] | null = null; - private readonly _workerHost = this._instantiationService.createInstance( - TextMateWorkerHost, - (timeMs, languageId, sourceExtensionId, lineLength, isRandomSample) => this.reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, true, isRandomSample) + private readonly _threadedBackgroundTokenizerFactory = this._instantiationService.createInstance( + ThreadedBackgroundTokenizerFactory, + (timeMs, languageId, sourceExtensionId, lineLength, isRandomSample) => this._reportTokenizationTime(timeMs, languageId, sourceExtensionId, lineLength, true, isRandomSample) ); constructor( @@ -77,7 +77,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate this._styleElement = dom.createStyleSheet(); this._styleElement.className = 'vscode-tokens-styles'; - grammarsExtPoint.setHandler((extensions) => this.handleGrammarsExtPoint(extensions)); + grammarsExtPoint.setHandler((extensions) => this._handleGrammarsExtPoint(extensions)); this._updateTheme(this._themeService.getColorTheme(), true); this._register(this._themeService.onDidColorThemeChange(() => { @@ -89,7 +89,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate }); } - private handleGrammarsExtPoint(extensions: readonly IExtensionPointUser[]): void { + private _handleGrammarsExtPoint(extensions: readonly IExtensionPointUser[]): void { this._grammarDefinitions = null; if (this._grammarFactory) { this._grammarFactory.dispose(); @@ -101,26 +101,26 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate for (const extension of extensions) { const grammars = extension.value; for (const grammar of grammars) { - const def = this.createValidGrammarDefinition(extension, grammar); - if (def) { - this._grammarDefinitions.push(def); - if (def.language) { - const lazyTokenizationSupport = new LazyTokenizationSupport(() => this.createTokenizationSupport(def.language!)); + const validatedGrammar = this._validateGrammarDefinition(extension, grammar); + if (validatedGrammar) { + this._grammarDefinitions.push(validatedGrammar); + if (validatedGrammar.language) { + const lazyTokenizationSupport = new LazyTokenizationSupport(() => this._createTokenizationSupport(validatedGrammar.language!)); this._tokenizersRegistrations.add(lazyTokenizationSupport); - this._tokenizersRegistrations.add(TokenizationRegistry.registerFactory(def.language, lazyTokenizationSupport)); + this._tokenizersRegistrations.add(TokenizationRegistry.registerFactory(validatedGrammar.language, lazyTokenizationSupport)); } } } } - this._workerHost.setGrammarDefinitions(this._grammarDefinitions); + this._threadedBackgroundTokenizerFactory.setGrammarDefinitions(this._grammarDefinitions); for (const createdMode of this._createdModes) { TokenizationRegistry.getOrCreate(createdMode); } } - private createValidGrammarDefinition(extension: IExtensionPointUser, grammar: ITMSyntaxExtensionPoint): IValidGrammarDefinition | null { + private _validateGrammarDefinition(extension: IExtensionPointUser, grammar: ITMSyntaxExtensionPoint): IValidGrammarDefinition | null { if (!validateGrammarExtensionPoint(extension.description.extensionLocation, grammar, extension.collector, this._languageService)) { return null; } @@ -162,10 +162,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate } } - let validLanguageId: string | null = null; - if (grammar.language && this._languageService.isRegisteredLanguageId(grammar.language)) { - validLanguageId = grammar.language; - } + const validLanguageId = grammar.language && this._languageService.isRegisteredLanguageId(grammar.language) ? grammar.language : null; function asStringArray(array: unknown, defaultValue: string[]): string[] { if (!Array.isArray(array)) { @@ -261,7 +258,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate return this._grammarFactory; } - private async createTokenizationSupport(languageId: string): Promise { + private async _createTokenizationSupport(languageId: string): Promise { if (!this._languageService.isRegisteredLanguageId(languageId)) { return null; } @@ -289,10 +286,10 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate r.grammar, r.initialState, r.containsEmbeddedLanguages, - (textModel, tokenStore) => this._workerHost.createBackgroundTokenizer(textModel, tokenStore, maxTokenizationLineLength), + (textModel, tokenStore) => this._threadedBackgroundTokenizerFactory.createBackgroundTokenizer(textModel, tokenStore, maxTokenizationLineLength), () => this._configurationService.getValue('editor.experimental.asyncTokenizationVerification'), (timeMs, lineLength, isRandomSample) => { - this.reportTokenizationTime(timeMs, languageId, r.sourceExtensionId, lineLength, false, isRandomSample); + this._reportTokenizationTime(timeMs, languageId, r.sourceExtensionId, lineLength, false, isRandomSample); }, true, ); @@ -329,11 +326,11 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate TokenizationRegistry.setColorMap(colorMap); if (this._currentTheme && this._currentTokenColorMap) { - this._workerHost.acceptTheme(this._currentTheme, this._currentTokenColorMap); + this._threadedBackgroundTokenizerFactory.acceptTheme(this._currentTheme, this._currentTokenColorMap); } } - public async createGrammar(languageId: string): Promise { + public async createTokenizer(languageId: string): Promise { if (!this._languageService.isRegisteredLanguageId(languageId)) { return null; } @@ -378,7 +375,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate } } - public reportTokenizationTime(timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, fromWorker: boolean, isRandomSample: boolean): void { + private _reportTokenizationTime(timeMs: number, languageId: string, sourceExtensionId: string | undefined, lineLength: number, fromWorker: boolean, isRandomSample: boolean): void { // 50 events per hour (one event has a low probability) if (TextMateTokenizationFeature.reportTokenizationTimeCounter > 50) { // Don't flood telemetry with too many events diff --git a/src/vs/workbench/services/tunnel/browser/tunnelService.ts b/src/vs/workbench/services/tunnel/browser/tunnelService.ts index ff97c93c22f82..74c874e0bfdc8 100644 --- a/src/vs/workbench/services/tunnel/browser/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/browser/tunnelService.ts @@ -8,7 +8,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; -import { AbstractTunnelService, ITunnelService, RemoteTunnel } from 'vs/platform/tunnel/common/tunnel'; +import { AbstractTunnelService, ITunnelProvider, ITunnelService, RemoteTunnel, isTunnelProvider } from 'vs/platform/tunnel/common/tunnel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class TunnelService extends AbstractTunnelService { @@ -24,15 +24,15 @@ export class TunnelService extends AbstractTunnelService { return false; } - protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, _localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { + protected retainOrCreateTunnel(tunnelProvider: IAddressProvider | ITunnelProvider, remoteHost: string, remotePort: number, _localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; } - if (this._tunnelProvider) { - return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); + if (isTunnelProvider(tunnelProvider)) { + return this.createWithProvider(tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); } return undefined; } diff --git a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts index 4e581d2e485e7..edd218d3a9c99 100644 --- a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts @@ -7,7 +7,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { URI } from 'vs/base/common/uri'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ITunnelService, AbstractTunnelService, RemoteTunnel, TunnelPrivacyId, isPortPrivileged } from 'vs/platform/tunnel/common/tunnel'; +import { ITunnelService, AbstractTunnelService, RemoteTunnel, TunnelPrivacyId, isPortPrivileged, ITunnelProvider, isTunnelProvider } from 'vs/platform/tunnel/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; import { ISharedProcessTunnelService } from 'vs/platform/remote/common/sharedProcessTunnelService'; @@ -79,19 +79,19 @@ export class TunnelService extends AbstractTunnelService { return isPortPrivileged(port, this.defaultTunnelHost, OS, this._nativeWorkbenchEnvironmentService.os.release); } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { + protected retainOrCreateTunnel(addressOrTunnelProvider: IAddressProvider | ITunnelProvider, remoteHost: string, remotePort: number, localHost: string, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; } - if (this._tunnelProvider) { - return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); + if (isTunnelProvider(addressOrTunnelProvider)) { + return this.createWithProvider(addressOrTunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); } else { this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`); - const tunnel = this._createSharedProcessTunnel(addressProvider, remoteHost, remotePort, localHost, localPort, elevateIfNeeded); + const tunnel = this._createSharedProcessTunnel(addressOrTunnelProvider, remoteHost, remotePort, localHost, localPort, elevateIfNeeded); this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.'); this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; diff --git a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css index efc8cd8c16b3b..aa12a2746204c 100644 --- a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css +++ b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css @@ -7,43 +7,43 @@ display: inherit; } -.monaco-workbench .pane > .pane-body > .profile-view-message-container { +.monaco-workbench .pane>.pane-body>.profile-view-message-container { display: flex; padding: 13px 20px 0px 20px; box-sizing: border-box; } -.monaco-workbench .pane > .pane-body > .profile-view-message-container p { +.monaco-workbench .pane>.pane-body>.profile-view-message-container p { margin-block-start: 0em; margin-block-end: 0em; } -.monaco-workbench .pane > .pane-body > .profile-view-message-container a { +.monaco-workbench .pane>.pane-body>.profile-view-message-container a { color: var(--vscode-textLink-foreground) } -.monaco-workbench .pane > .pane-body > .profile-view-message-container a:hover { +.monaco-workbench .pane>.pane-body>.profile-view-message-container a:hover { text-decoration: underline; color: var(--vscode-textLink-activeForeground) } -.monaco-workbench .pane > .pane-body > .profile-view-message-container a:active { +.monaco-workbench .pane>.pane-body>.profile-view-message-container a:active { color: var(--vscode-textLink-activeForeground) } -.monaco-workbench .pane > .pane-body > .profile-view-message-container.hide { +.monaco-workbench .pane>.pane-body>.profile-view-message-container.hide { display: none; } -.monaco-workbench .pane > .pane-body > .profile-view-buttons-container { +.monaco-workbench .pane>.pane-body>.profile-view-buttons-container { display: flex; flex-direction: column; padding: 13px 20px; box-sizing: border-box; } -.monaco-workbench .pane > .pane-body > .profile-view-buttons-container > .monaco-button, -.monaco-workbench .pane > .pane-body > .profile-view-buttons-container > .monaco-button-dropdown { +.monaco-workbench .pane>.pane-body>.profile-view-buttons-container>.monaco-button, +.monaco-workbench .pane>.pane-body>.profile-view-buttons-container>.monaco-button-dropdown { margin-block-start: 13px; margin-inline-start: 0px; margin-inline-end: 0px; @@ -52,12 +52,36 @@ margin-right: auto; } -.monaco-workbench .pane > .pane-body > .profile-view-buttons-container > .monaco-button-dropdown { +.monaco-workbench .pane>.pane-body>.profile-view-buttons-container>.monaco-button-dropdown { width: 100%; } -.monaco-workbench .pane > .pane-body > .profile-view-buttons-container > .monaco-button-dropdown > .monaco-dropdown-button { +.monaco-workbench .pane>.pane-body>.profile-view-buttons-container>.monaco-button-dropdown>.monaco-dropdown-button { display: flex; align-items: center; padding: 0 4px; } + +.profile-type-widget { + display: flex; + margin: 0px 6px 8px 11px; + align-items: center; + justify-content: space-between; + font-size: 12px; +} + +.profile-type-widget>.profile-type-select-container { + overflow: hidden; + padding-left: 10px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.profile-type-widget>.profile-type-select-container>.monaco-select-box { + cursor: pointer; + line-height: 17px; + padding: 2px 23px 2px 8px; + border-radius: 2px; +} diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index e45feb0cc50d6..1cb00b4da7c27 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -18,7 +18,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptorService, IViewsRegistry, IViewsService, TreeItemCollapsibleState, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, ProfileResourceType, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, ProfileResourceType, UseDefaultProfileFlags, isUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -35,19 +35,19 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { Button } from 'vs/base/browser/ui/button/button'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { generateUuid } from 'vs/base/common/uuid'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorsOrder } from 'vs/workbench/common/editor'; import { getErrorMessage, onUnexpectedError } from 'vs/base/common/errors'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { VSBuffer } from 'vs/base/common/buffer'; import { joinPath } from 'vs/base/common/resources'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -70,6 +70,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { MarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConstants'; +import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; interface IUserDataProfileTemplate { readonly name: string; @@ -129,6 +130,8 @@ export class UserDataProfileImportExportService extends Disposable implements IU @IURLService urlService: IURLService, @IProductService private readonly productService: IProductService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IContextViewService private readonly contextViewService: IContextViewService, @ILogService private readonly logService: ILogService, ) { super(); @@ -220,6 +223,225 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } + createProfile(from?: IUserDataProfile | URI): Promise { + return this.saveProfile(undefined, from); + } + + editProfile(profile: IUserDataProfile): Promise { + return this.saveProfile(profile); + } + + private saveProfile(profile: IUserDataProfile): Promise; + private saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | URI | IUserDataProfileTemplate): Promise; + private async saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | URI | Mutable): Promise { + + type SaveProfileInfoClassification = { + owner: 'sandy081'; + comment: 'Report when profile is about to be saved'; + }; + type CreateProfileInfoClassification = { + owner: 'sandy081'; + comment: 'Report when profile is about to be created'; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of profile source' }; + }; + type CreateProfileInfoEvent = { + source: string | undefined; + }; + const createProfileTelemetryData: CreateProfileInfoEvent = { source: source instanceof URI ? 'template' : isUserDataProfile(source) ? 'profile' : source ? 'external' : undefined }; + + if (profile) { + this.telemetryService.publicLog2<{}, SaveProfileInfoClassification>('userDataProfile.startEdit'); + } else { + this.telemetryService.publicLog2('userDataProfile.startCreate', createProfileTelemetryData); + } + + const disposables = new DisposableStore(); + const title = profile ? localize('save profile', "Edit {0} Profile...", profile.name) : localize('create new profle', "Create New Profile..."); + + const settings: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Settings, label: localize('settings', "Settings"), picked: !profile?.useDefaultFlags?.settings }; + const keybindings: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Keybindings, label: localize('keybindings', "Keyboard Shortcuts"), picked: !profile?.useDefaultFlags?.keybindings }; + const snippets: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Snippets, label: localize('snippets', "User Snippets"), picked: !profile?.useDefaultFlags?.snippets }; + const tasks: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Tasks, label: localize('tasks', "User Tasks"), picked: !profile?.useDefaultFlags?.tasks }; + const extensions: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Extensions, label: localize('extensions', "Extensions"), picked: !profile?.useDefaultFlags?.extensions }; + const resources = [settings, keybindings, snippets, tasks, extensions]; + + const quickPick = this.quickInputService.createQuickPick(); + quickPick.title = title; + quickPick.placeholder = localize('name placeholder', "Profile name"); + quickPick.value = profile?.name ?? (isUserDataProfileTemplate(source) ? this.generateProfileName(source.name) : ''); + quickPick.canSelectMany = true; + quickPick.matchOnDescription = false; + quickPick.matchOnDetail = false; + quickPick.matchOnLabel = false; + quickPick.sortByLabel = false; + quickPick.hideCountBadge = true; + quickPick.ok = false; + quickPick.customButton = true; + quickPick.hideCheckAll = true; + quickPick.ignoreFocusOut = true; + quickPick.customLabel = profile ? localize('save', "Save") : localize('create', "Create"); + quickPick.description = localize('customise the profile', "Choose what to configure in your Profile:"); + quickPick.items = [...resources]; + + const update = () => { + quickPick.items = resources; + quickPick.selectedItems = resources.filter(item => item.picked); + }; + update(); + + const validate = () => { + if (!profile && this.userDataProfilesService.profiles.some(p => p.name === quickPick.value)) { + quickPick.validationMessage = localize('profileExists', "Profile with name {0} already exists.", quickPick.value); + quickPick.severity = Severity.Warning; + return; + } + if (resources.every(resource => !resource.picked)) { + quickPick.validationMessage = localize('invalid configurations', "The profile should contain at least one configuration."); + quickPick.severity = Severity.Warning; + return; + } + quickPick.severity = Severity.Ignore; + quickPick.validationMessage = undefined; + }; + + disposables.add(quickPick.onDidChangeSelection(items => { + let needUpdate = false; + for (const resource of resources) { + resource.picked = items.includes(resource); + const description = resource.picked ? undefined : localize('use default profile', "Using Default Profile"); + if (resource.description !== description) { + resource.description = description; + needUpdate = true; + } + } + if (needUpdate) { + update(); + } + validate(); + })); + + disposables.add(quickPick.onDidChangeValue(validate)); + + let result: { name: string; items: ReadonlyArray } | undefined; + disposables.add(Event.any(quickPick.onDidCustom, quickPick.onDidAccept)(() => { + if (!quickPick.value) { + quickPick.validationMessage = localize('name required', "Provide a name for the new profile"); + quickPick.severity = Severity.Error; + } + if (quickPick.validationMessage) { + return; + } + result = { name: quickPick.value, items: quickPick.selectedItems }; + quickPick.hide(); + quickPick.severity = Severity.Ignore; + quickPick.validationMessage = undefined; + })); + + if (!profile && !isUserDataProfileTemplate(source)) { + const domNode = DOM.$('.profile-type-widget'); + DOM.append(domNode, DOM.$('.profile-type-create-label', undefined, localize('create from', "Copy from:"))); + const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; + const profileOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] = []; + profileOptions.push({ text: localize('empty profile', "None") }); + const templates = await this.userDataProfileManagementService.getBuiltinProfileTemplates(); + if (templates.length) { + profileOptions.push({ ...separator, decoratorRight: localize('from templates', "Profile Templates") }); + for (const template of templates) { + profileOptions.push({ text: template.name, id: template.url, source: URI.parse(template.url) }); + } + } + profileOptions.push({ ...separator, decoratorRight: localize('from existing profiles', "Existing Profiles") }); + for (const profile of this.userDataProfilesService.profiles) { + profileOptions.push({ text: profile.name, id: profile.id, source: profile }); + } + + const findOptionIndex = () => { + const index = profileOptions.findIndex(option => { + if (source instanceof URI) { + return option.source instanceof URI && this.uriIdentityService.extUri.isEqual(option.source, source); + } else if (isUserDataProfile(source)) { + return option.id === source.id; + } + return false; + }); + return index > -1 ? index : 0; + }; + + const initialIndex = findOptionIndex(); + const selectBox = disposables.add(this.instantiationService.createInstance(SelectBox, profileOptions, initialIndex, this.contextViewService, defaultSelectBoxStyles, { useCustomDrawn: true })); + selectBox.render(DOM.append(domNode, DOM.$('.profile-type-select-container'))); + quickPick.widget = domNode; + + if (profileOptions[initialIndex].source) { + quickPick.value = this.generateProfileName(profileOptions[initialIndex].text); + } + + const updateOptions = () => { + const option = profileOptions[findOptionIndex()]; + for (const resource of resources) { + resource.picked = option.source && !(option.source instanceof URI) ? !option.source?.useDefaultFlags?.[resource.id] : true; + } + update(); + }; + + updateOptions(); + disposables.add(selectBox.onDidSelect(({ index }) => { + source = profileOptions[index].source; + updateOptions(); + })); + } + + quickPick.show(); + + await new Promise((c, e) => { + disposables.add(quickPick.onDidHide(() => { + disposables.dispose(); + c(); + })); + }); + + if (!result) { + if (profile) { + this.telemetryService.publicLog2<{}, SaveProfileInfoClassification>('userDataProfile.cancelEdit'); + } else { + this.telemetryService.publicLog2('userDataProfile.cancelCreate', createProfileTelemetryData); + } + return; + } + + try { + const useDefaultFlags: UseDefaultProfileFlags | undefined = result.items.length === resources.length + ? undefined + : { + settings: !result.items.includes(settings), + keybindings: !result.items.includes(keybindings), + snippets: !result.items.includes(snippets), + tasks: !result.items.includes(tasks), + extensions: !result.items.includes(extensions) + }; + if (profile) { + await this.userDataProfileManagementService.updateProfile(profile, { name: result.name, useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); + } else { + if (source instanceof URI) { + this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); + await this.importProfile(source, { mode: 'apply', name: result.name, useDefaultFlags }); + } else if (isUserDataProfile(source)) { + this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); + await this.createFromProfile(source, result.name, { useDefaultFlags }); + } else if (isUserDataProfileTemplate(source)) { + source.name = result.name; + this.telemetryService.publicLog2('userDataProfile.createFromExternalTemplate', createProfileTelemetryData); + await this.createAndSwitch(source, false, true, { useDefaultFlags }, localize('create profile', "Create Profile")); + } else { + this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); + await this.userDataProfileManagementService.createAndEnterProfile(result.name, { useDefaultFlags }); + } + } + } catch (error) { + this.notificationService.error(error); + } + } + async showProfileContents(): Promise { const view = this.viewsService.getViewWithId(EXPORT_PROFILE_PREVIEW_VIEW); if (view) { @@ -249,11 +471,25 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - async createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileOptions): Promise { + private async createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileOptions): Promise { const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, profile); try { const profileTemplate = await userDataProfilesExportState.getProfileTemplate(name, undefined); - await this.createAndSwitch(profileTemplate, false, true, options, localize('create profile', "Create Profile")); + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + delay: 500, + sticky: true, + }, async progress => { + const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); + const profile = await this.doCreateProfile(profileTemplate, false, false, { useDefaultFlags: options?.useDefaultFlags }, reportProgress); + if (profile) { + reportProgress(localize('progress extensions', "Applying Extensions...")); + await this.instantiationService.createInstance(ExtensionsResource).copy(this.userDataProfileService.currentProfile, profile, false); + + reportProgress(localize('switching profile', "Switching Profile...")); + await this.userDataProfileManagementService.switchProfile(profile); + } + }); } finally { userDataProfilesExportState.dispose(); } @@ -269,7 +505,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU sticky: true, }, async progress => { const reportProgress = (message: string) => progress.report({ message: localize('troubleshoot profile progress', "Setting up Troubleshoot Profile: {0}", message) }); - const profile = await this.createProfile(profileTemplate, true, false, { useDefaultFlags: this.userDataProfileService.currentProfile.useDefaultFlags }, reportProgress); + const profile = await this.doCreateProfile(profileTemplate, true, false, { useDefaultFlags: this.userDataProfileService.currentProfile.useDefaultFlags }, reportProgress); if (profile) { reportProgress(localize('progress extensions', "Applying Extensions...")); await this.instantiationService.createInstance(ExtensionsResource).copy(this.userDataProfileService.currentProfile, profile, true); @@ -380,7 +616,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } const barrier = new Barrier(); - const importAction = this.getCreateAction(barrier, userDataProfileImportState, options); + const importAction = this.getCreateAction(barrier, userDataProfileImportState); const primaryAction = isWeb ? new Action('importInDesktop', localize('import in desktop', "Create Profile in {0}", this.productService.nameLong), undefined, true, async () => this.openerService.open(uri, { openExternal: true })) : importAction; @@ -412,12 +648,12 @@ export class UserDataProfileImportExportService extends Disposable implements IU return that.progressService.withProgress({ location: IMPORT_PROFILE_PREVIEW_VIEW, }, async progress => { - disposable.dispose(); view.setMessage(undefined); const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport(); if (profileTemplate.extensions) { await that.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, importedProfile); } + disposable.dispose(); }); } })); @@ -447,8 +683,9 @@ export class UserDataProfileImportExportService extends Disposable implements IU await this.createAndSwitch(profileTemplate, false, true, options, localize('create profile', "Create Profile")); } else { const barrier = new Barrier(); - const importAction = this.getCreateAction(barrier, userDataProfileImportState, options); - await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, profileTemplate.name, importAction, new BarrierAction(barrier, new Action('cancel', localize('cancel', "Cancel")), this.notificationService), false, userDataProfileImportState); + const cancelAction = new BarrierAction(barrier, new Action('cancel', localize('cancel', "Cancel")), this.notificationService); + const importAction = this.getCreateAction(barrier, userDataProfileImportState, cancelAction); + await this.showProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW, profileTemplate.name, importAction, cancelAction, false, userDataProfileImportState); await barrier.wait(); await this.hideProfilePreviewView(IMPORT_PROFILE_PREVIEW_VIEW); } @@ -457,11 +694,14 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - private getCreateAction(barrier: Barrier, userDataProfileImportState: UserDataProfileImportState, options: IUserDataProfileOptions | undefined): IAction { + private getCreateAction(barrier: Barrier, userDataProfileImportState: UserDataProfileImportState, cancelAction?: IAction): IAction { const importAction = new BarrierAction(barrier, new Action('title', localize('import', "Create Profile"), undefined, true, async () => { importAction.enabled = false; + if (cancelAction) { + cancelAction.enabled = false; + } const profileTemplate = await userDataProfileImportState.getProfileTemplateToImport(); - return this.createAndSwitch(profileTemplate, false, true, options, localize('create profile', "Create Profile")); + return this.saveProfile(undefined, profileTemplate); }), this.notificationService); return importAction; } @@ -475,7 +715,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU title = `${title} (${profileTemplate.name})`; progress.report({ message: title }); const reportProgress = (message: string) => progress.report({ message: `${title}: ${message}` }); - const profile = await this.createProfile(profileTemplate, temporaryProfile, extensions, options, reportProgress); + const profile = await this.doCreateProfile(profileTemplate, temporaryProfile, extensions, options, reportProgress); if (profile) { reportProgress(localize('switching profile', "Switching Profile...")); await this.userDataProfileManagementService.switchProfile(profile); @@ -484,33 +724,33 @@ export class UserDataProfileImportExportService extends Disposable implements IU }); } - private async createProfile(profileTemplate: IUserDataProfileTemplate, temporaryProfile: boolean, extensions: boolean, options: IUserDataProfileOptions | undefined, progress: (message: string) => void): Promise { + private async doCreateProfile(profileTemplate: IUserDataProfileTemplate, temporaryProfile: boolean, extensions: boolean, options: IUserDataProfileOptions | undefined, progress: (message: string) => void): Promise { const profile = await this.getProfileToImport(profileTemplate, temporaryProfile, options); if (!profile) { return undefined; } - if (profileTemplate.settings) { + if (profileTemplate.settings && !profile.useDefaultFlags?.settings) { progress(localize('progress settings', "Applying Settings...")); await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); } - if (profileTemplate.keybindings) { + if (profileTemplate.keybindings && !profile.useDefaultFlags?.keybindings) { progress(localize('progress keybindings', "Applying Keyboard Shortcuts...")); await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); } - if (profileTemplate.tasks) { + if (profileTemplate.tasks && !profile.useDefaultFlags?.tasks) { progress(localize('progress tasks', "Applying Tasks...")); await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); } - if (profileTemplate.snippets) { + if (profileTemplate.snippets && !profile.useDefaultFlags?.snippets) { progress(localize('progress snippets', "Applying Snippets...")); await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); } - if (profileTemplate.globalState) { + if (profileTemplate.globalState && !profile.useDefaultFlags?.globalState) { progress(localize('progress global state', "Applying State...")); await this.instantiationService.createInstance(GlobalStateResource).apply(profileTemplate.globalState, profile); } - if (profileTemplate.extensions && extensions) { + if (profileTemplate.extensions && extensions && !profile.useDefaultFlags?.extensions) { progress(localize('progress extensions', "Applying Extensions...")); await this.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, profile); } @@ -633,6 +873,11 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } + private generateProfileName(profileName: string): string { + const existingProfile = this.userDataProfilesService.profiles.find(p => p.name === profileName); + return existingProfile ? `${profileName} ${this.getProfileNameIndex(profileName)}` : profileName; + } + private getProfileNameIndex(name: string): number { const nameRegEx = new RegExp(`${escapeRegExpCharacters(name)}\\s(\\d+)`); let nameIndex = 0; @@ -898,9 +1143,9 @@ abstract class UserDataProfileImportExportState extends Disposable implements IT const children = await (element).getChildren(); if (children) { for (const child of children) { - child.checkbox = child.parent.checkbox && child.checkbox - ? { ...child.checkbox, isChecked: child.parent.checkbox.isChecked ? child.checkbox.isChecked : false } - : undefined; + if (child.parent.checkbox && child.checkbox) { + child.checkbox.isChecked = child.parent.checkbox.isChecked && child.checkbox.isChecked; + } } } return children; @@ -982,7 +1227,10 @@ abstract class UserDataProfileImportExportState extends Disposable implements IT } private isSelected(treeItem: IProfileResourceTreeItem): boolean { - return treeItem.checkbox?.isChecked ?? true; + if (treeItem.checkbox) { + return treeItem.checkbox.isChecked || !!treeItem.children?.some(child => child.checkbox?.isChecked ?? true); + } + return true; } protected abstract fetchRoots(): Promise; diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index 9468c15aee2d9..33757af0467cb 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -3,18 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IRequestService, asJson } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkspaceContextService, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { DidChangeUserDataProfileEvent, IUserDataProfileManagementService, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { DidChangeUserDataProfileEvent, IProfileTemplateInfo, IUserDataProfileManagementService, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export type ProfileManagementActionExecutedClassification = { owner: 'sandy081'; @@ -38,6 +42,9 @@ export class UserDataProfileManagementService extends Disposable implements IUse @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IProductService private readonly productService: IProductService, + @IRequestService private readonly requestService: IRequestService, + @ILogService private readonly logService: ILogService, ) { super(); this._register(userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); @@ -120,6 +127,22 @@ export class UserDataProfileManagementService extends Disposable implements IUse this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'switchProfile' }); } + async getBuiltinProfileTemplates(): Promise { + if (this.productService.profileTemplatesUrl) { + try { + const context = await this.requestService.request({ type: 'GET', url: this.productService.profileTemplatesUrl }, CancellationToken.None); + if (context.res.statusCode === 200) { + return (await asJson(context)) || []; + } else { + this.logService.error('Could not get profile templates.', context.res.statusCode); + } + } catch (error) { + this.logService.error(error); + } + } + return []; + } + private async changeCurrentProfile(profile: IUserDataProfile, reloadMessage?: string): Promise { const isRemoteWindow = !!this.environmentService.remoteAuthority; diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index d68ed2e329e8e..25347a80b98ed 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -33,6 +33,11 @@ export interface IUserDataProfileService { getShortName(profile: IUserDataProfile): string; } +export interface IProfileTemplateInfo { + readonly name: string; + readonly url: string; +} + export const IUserDataProfileManagementService = createDecorator('IUserDataProfileManagementService'); export interface IUserDataProfileManagementService { readonly _serviceBrand: undefined; @@ -42,6 +47,7 @@ export interface IUserDataProfileManagementService { removeProfile(profile: IUserDataProfile): Promise; updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise; switchProfile(profile: IUserDataProfile): Promise; + getBuiltinProfileTemplates(): Promise; } @@ -87,7 +93,8 @@ export interface IUserDataProfileImportExportService { exportProfile(): Promise; importProfile(uri: URI, options?: IProfileImportOptions): Promise; showProfileContents(): Promise; - createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileOptions): Promise; + createProfile(from?: IUserDataProfile | URI): Promise; + editProfile(profile: IUserDataProfile): Promise; createTroubleshootProfile(): Promise; setProfile(profile: IUserDataProfileTemplate): Promise; } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index b350719272050..7b9f31a10e456 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1435,6 +1435,9 @@ export class TestHostService implements IHostService { async restart(): Promise { } async reload(): Promise { } async close(): Promise { } + async withExpectedShutdown(expectedShutdownTask: () => Promise): Promise { + return await expectedShutdownTask(); + } async focus(options?: { force: boolean }): Promise { } diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index b83e3ab1da151..c9fda66ae715d 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -53,7 +53,8 @@ declare module 'vscode' { export enum InteractiveEditorResponseFeedbackKind { Unhelpful = 0, Helpful = 1, - Undone = 2 + Undone = 2, + Accepted = 3 } export interface TextDocumentContext { @@ -125,7 +126,12 @@ declare module 'vscode' { responseId: string; } - export type InteractiveProgress = InteractiveProgressContent | InteractiveProgressId; + export interface InteractiveProgressTask { + placeholder: string; + resolvedContent: Thenable; + } + + export type InteractiveProgress = InteractiveProgressContent | InteractiveProgressId | InteractiveProgressTask; export interface InteractiveResponseCommand { commandId: string; diff --git a/src/vscode-dts/vscode.proposed.terminalSelection.d.ts b/src/vscode-dts/vscode.proposed.terminalSelection.d.ts new file mode 100644 index 0000000000000..706bcdafe104f --- /dev/null +++ b/src/vscode-dts/vscode.proposed.terminalSelection.d.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. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/188173 + + export interface Terminal { + /** + * The selected text of the terminal or undefined if there is no selection. + */ + readonly selection: string | undefined; + } +} diff --git a/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts b/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts new file mode 100644 index 0000000000000..ad4655d9bcae8 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.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. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface TreeView2 extends Disposable { + readonly onDidExpandElement: Event>; + readonly onDidCollapseElement: Event>; + readonly selection: readonly T[]; + readonly onDidChangeSelection: Event>; + readonly visible: boolean; + readonly onDidChangeVisibility: Event; + readonly onDidChangeCheckboxState: Event>; + title?: string; + description?: string; + badge?: ViewBadge | undefined; + reveal(element: T, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }): Thenable; + + /** + * An optional human-readable message that will be rendered in the view. + * Only a subset of markdown is supported. + * Setting the message to null, undefined, or empty string will remove the message from the view. + */ + message?: string | MarkdownString; + } +} diff --git a/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts b/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts new file mode 100644 index 0000000000000..5a2192df3c8c7 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.tunnelFactory.d.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. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + /** + * Used as part of the ResolverResult if the extension has any candidate, + * published, or forwarded ports. + */ + export interface TunnelInformation { + /** + * Tunnels that are detected by the extension. The remotePort is used for display purposes. + * The localAddress should be the complete local address (ex. localhost:1234) for connecting to the port. Tunnels provided through + * environmentTunnels are read-only from the forwarded ports UI. + */ + environmentTunnels?: TunnelDescription[]; + + tunnelFeatures?: { + elevation: boolean; + /** + * One of the the options must have the ID "private". + */ + privacyOptions: TunnelPrivacy[]; + }; + } + + export interface TunnelProvider { + /** + * Provides port forwarding capabilities. If there is a resolver that already provids tunnels, then the resolver's provider will + * be used. If multiple providers are registered, then only the first will be used. + */ + provideTunnel(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions, token: CancellationToken): ProviderResult; + } + + export namespace workspace { + /** + * Registering a tunnel provider enables port forwarding. This will cause the Ports view to show. + * @param provider + */ + export function registerTunnelProvider(provider: TunnelProvider, information: TunnelInformation): Thenable; + } + +} diff --git a/yarn.lock b/yarn.lock index 4983356750544..651bda6187cae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1001,6 +1001,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/kerberos@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/kerberos/-/kerberos-1.1.2.tgz#2a774abd48f727852f697d74241e9de3aea8e646" + integrity sha512-cLixfcXjdj7qohLasmC1G4fh+en4e4g7mFZiG38D+K9rS9BRKFlq1JH5dGkQzICckbu4wM+RcwSa4VRHlBg7Rg== + "@types/keytar@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.0.tgz#ca24e6ee6d0df10c003aafe26e93113b8faf0d8e" @@ -1304,10 +1309,10 @@ bindings "^1.5.0" node-addon-api "^6.0.0" -"@vscode/proxy-agent@^0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.16.0.tgz#32054387f7aaf26d1b5d53f553d53bfd8489eab8" - integrity sha512-b8yBHgdngDrP+9HPJtnPUJjPHd+zfEvOYoc8KioWJVs0rFVT2U77nFDVC70Mrrscf87ya2a/sPY32nTrwFfOQQ== +"@vscode/proxy-agent@^0.17.1": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.17.1.tgz#00ea42fb3565c78c38bc99a73d4460db538aef4e" + integrity sha512-KWQ5y2uB6547Oudx2TMV28PdcdqNzI4J7TZzhZht1kNra8spqOzQJXw6gBdoh2mMFVpNiKgVhZ9YinWR0BZHiw== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -3582,10 +3587,10 @@ electron-to-chromium@^1.4.202: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.207.tgz#9c3310ebace2952903d05dcaba8abe3a4ed44c01" integrity sha512-piH7MJDJp4rJCduWbVvmUd59AUne1AFBJ8JaRQvk0KzNTSUnZrVXHCZc+eg+CGE4OujkcLJznhGKD6tuAshj5Q== -electron@22.3.14: - version "22.3.14" - resolved "https://registry.yarnpkg.com/electron/-/electron-22.3.14.tgz#539fc7d7b6df37483aaa351856a28e43092d550e" - integrity sha512-WxVcLnC4DrkBLN1/BwpxNkGvVq8iq1hM7lae5nvjnSYg/bwVbuo1Cwc80Keft4MIWKlYCXNiKKqs3qCXV4Aiaw== +electron@22.3.17: + version "22.3.17" + resolved "https://registry.yarnpkg.com/electron/-/electron-22.3.17.tgz#90a75f78cc761ed536d8210dd001e142fca78691" + integrity sha512-mo9qD1pOkiibvH+pgETsq9RZF0p3O5ACwxzjk3E2ozMYb9cnJenZyE3jxbs4WqzDCFi+rsm6WWahw3hlPhANXw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^16.11.26" @@ -6205,6 +6210,15 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== +kerberos@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.0.1.tgz#663b0b46883b4da84495f60f2e9e399a43a33ef5" + integrity sha512-O/jIgbdGK566eUhFwIcgalbqirYU/r76MW7/UFw06Fd9x5bSwgyZWL/Vm26aAmezQww/G9KYkmmJBkEkPk5HLw== + dependencies: + bindings "^1.5.0" + node-addon-api "^4.3.0" + prebuild-install "7.1.1" + keygrip@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" @@ -8194,6 +8208,24 @@ postcss@^8.4.19: picocolors "^1.0.0" source-map-js "^1.0.2" +prebuild-install@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prebuild-install@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870"